Управление инфраструктурой Open Telekom Cloud с помощью Ansible

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Open Telekom Cloud

В этой статье расскажу о нашем опыте работы над развитием инструментов управления инфраструктурой облачного сервиса Open Telekom Cloud, как мы столкнулись с особенностями этого облака, какие решения принимали и какие инструменты использовали.

Open Telekom Cloud – международная публичная облачная платформа, основанная на OpenStack. Платформа идеально подходит для компаний или стартапов, которые работают с европейскими пользователями, чьи персональные данные должны храниться в пределах Евросоюза: сервис разработан Deutsche Telekom и соответствует стандартам защиты данных GDPR (Генеральный регламент о защите персональных данных) EC.

С чего все начиналось

Почти два года назад в поисках специалистов в Россию пришел проект Open Telekom Cloud. Требовалось много людей на автоматизированное тестирование и несколько человек в спецотряд под названием Ecosystems, требования были очень расплывчатые: «Ну, надо знать Python и понимать, как работать с облачными сервисами…»

В то время, по удачному стечению обстоятельств, завершалось несколько проектов в Воронеже, и около 10 человек были готовы к взятию новых барьеров. Самых опытных из них отправили в команду Ecosystems, остальные отправились в QA.

Команда Ecosystems занималась API мониторингом, мониторингом сервисов, созданием модулей для Ansible, разработкой и поддержкой инструментов управления инфраструктурой Open Telekom Cloud. На данный момент она участвует еще и в разработке Terraform Provider, OpenStack SDK, OpenStack Ansible Collections. Во всех наших инструментах (OTC Extensions, Terraform Provider, Ansible Collections) мы стараемся максимально соответствовать OpenStack и переиспользовать существующие решения для него.

С самого начала с Open Telekom Cloud все оказалось довольно интересно. Разработка находится на стороне Huawei, декларировалось, что облако основано полностью на технологии OpenStack. Но Huawei внесли множество своих решений в сервисы. Многие из них были полностью написаны китайскими коллегами, были заметные отличия нашего API от OpenStack API.

Но тогда это нас не сильно волновало. Первой нашей задачей в Ecosystems было создание мониторингов, которые помогут определить качество работы тех или иных сервисов, в сложных сценариях. Например, использовать балансировщик нагрузки для хостов в разных AZ (availability zone), наблюдать за распределением запросов по каждому из хостов. Или следить за отказоустойчивостью того же балансировщика нагрузки в сценарии, когда один или несколько хостов за ним выключаются по тем или иным причинам.

В качестве инструментов для реализации задачи был выбран Ansible и Terraform, провайдер для Terraform уже существовал, и Huawei его в какой-то степени поддерживал. При создании мониторинга начали изучать и использовать Go для различных задач. Например, нужен был быстрый сервер, который не загнется от потока запросов, что в будущем открыло нам новое направление с поддержкой провайдера Terraform. Во время создания мониторинга находились баги в Terraform провайдере, и казалось, что дождаться их решения будет невозможно, никто их исправлять не спешил.

Что ж, раз Terraform стал для нас критичным инструментом, мы отправились в репозиторий с провайдером и начали исправлять баги. В какой-то момент поддержка провайдера со стороны Huawei прекратилась, да и через некоторое время HashiCorp решили всех сторонних провайдеров убрать из своего репозитория.

Тогда мы решили перетащить провайдер в свою организацию на Github, форкаем golangsdk, называем его gophertelekomcloud и переносим провайдер на него (после всех преобразований gophertelekomcloud в итоге стал самостоятельным проектом, указания, что это форк, больше нет). Но это уже другая история…

С начала работы на проекте прошло примерно полгода, из-за провайдера объем работы вырос и стало понятно, что два человека со всем не справятся. Мы набрали в команду толковых ребят, и часть из них стала заниматься развитием и поддержкой Terraform провайдера, часть осталась на мониторингах.

Ansible и коллекции

Опустив некоторые детали, первоначально мониторинг работал примерно так:

Точкой входа был AWX, он вызывал плейбуки с ролью Terraform, результат выполнения Terraform снова передавался в Ansible, и далее выполнялись какие-то сценарии.

Кажется, что все отлично, есть базовый .tf модуль, который создает сетевую часть, и отдельные модули на каждый сценарий, .state всех модулей хранится в s3. Все удобно, работает как часы.

Но подход к мониторингу поменялся. Мы начинали как самостоятельный проект без ограничений на выбор инструментов и не было задачи интегрироваться в какую-то существующую инфраструктуру, но теперь пришла задача интегрироваться в существующую инфраструктуру API-мониторинга с целью сделать более универсальный единый инструмент. В которой нет AWX и Terraform.

Был только Python и Ansible…

Учитывая, что Open Telekom Cloud не является решением, на 100% совместимым с OpenStack, в нем присутствуют сервисы собственной разработки, например, RDS (Relational Database Service). С помощью Ansible мы не можем построить все необходимые нам ресурсы используя OpenStack SDK и ansible-collections-openstack, в таком виде, чтобы это было легко поддерживать.

Что ж, надо расширять возможности OpenStack SDK, адаптировать под наш проект и писать собственные коллекции. Для коллекций необходимо описание ресурсов, которых нет в OpenStack SDK, для таких ресурсов был создан проект OTC Extensions.

OTC Extensions

Этот проект дополняет и расширяет функции OpenStack SDK для работы с Open Telekom Cloud, так же если он установлен в качестве python package, в OpenStack Client добавляются дополнительные плагины для работы с облаком.

Взаимодействует с:

·         python-openstacksdk

·         python-openstackclient

Структура проекта близка к OpenStack SDK:

otcextensions/
    sdk/
        compute/
            v2/
                server.py
                _proxy.py
    tests/
        unit/
            sdk/
                compute/
                    v2/
                        test_server.py

Все дополнительные ресурсы унаследованы от базового openstack.resource.Resource, или если мы хотим изменить существующий объект то нужно наследование от него базового класса этого объекта, например, если у openstack.compute.v2.server нет поддержки тэгов или они реализованы иначе:

class Server(server.Server):

    def add_tag(self, session, tag):
        """Adds a single tag to the resource."""

    def remove_tag(self, session, tag):
        """Removes a single tag from the specified server."""

И далее патчим Connection в методе load (otcextensions/sdk/__init__.py):

openstack.compute.v2.server.Server.add_tag = server.Server.add_tag
openstack.compute.v2.server.Server.remove_tag = server.Server.remove_tag

В итоге наш connection теперь будет работать с кастомными тегами.

Для нового ресурса:

otcextensions/
    sdk/
        elb/
            v2/
                elb_certificate.py
                _proxy.py

В файле elb_certificate.py создаем класс, указываем его url, какие методы поддерживает, какие параметры принимает

class Certificate(resource.Resource):
resources_key = 'certificates'
base_path = ('/lbaas/certificates')

# capabilities
allow_create = True
allow_fetch = True
allow_commit = True
allow_delete = True
allow_list = True

_query_mapping = resource.QueryParameters(
    'id', 'name', 'description',
    'type', 'domain', 'content',
    'private_key', 'marker', 'limit',
)

# Properties
#: Name
name = resource.Body('name')
#: Id
id = resource.Body('id')
#: Description
description = resource.Body('description')
#: Certificate type.
type = resource.Body('type')
#: Domain name associated with the server certificate.
domain = resource.Body('domain')
#: Private key of the server certificate. *Type: string*
private_key = resource.Body('private_key')
#: Public key of the server certificate or CA certificate. *Type: string*
content = resource.Body('certificate')
#: Administrative status of the certificate.
admin_state_up = resource.Body('admin_state_up')
#: Creation time
create_time = resource.Body('create_time')
#: Specifies the project ID.
project_id = resource.Body('tenant_id')
#: Time when the certificate expires.
expire_time = resource.Body('expire_time')
#: Time when the certificate was updated.
update_time = resource.Body('update_time')

Рядом обязательно должен быть файл _proxy.py, этот класс адаптер предоставляет интерфейс для работы с инстансом Connection, в нем мы описываем методы ресурса:

class Proxy(_proxy.Proxy):
    skip_discovery = True

    # ======== Certificate ========
    def create_certificate(self, **attrs):
        return self._create(_certificate.Certificate, **attrs)

    def certificates(self, **query):
        return self._list(_certificate.Certificate, **query)

    def delete_certificate(self, certificate, ignore_missing=True):
        return self._delete(_certificate.Certificate, certificate,
                            ignore_missing=ignore_missing)

    def get_certificate(self, certificate):
        return self._get(_certificate.Certificate, certificate)

    def update_certificate(self, certificate, **attrs):
        return self._update(_certificate.Certificate, certificate, **attrs)

    def find_certificate(self, name_or_id, ignore_missing=False):
        return self._find(_certificate.Certificate, name_or_id,
                          ignore_missing=ignore_missing)

В otcextensions/sdk/__init__.py eсть структура со всеми нестандартными ресурсами - OTC_SERVICES, добавляем наш ресурс по имени папки в которой он находится:

'elb': {
    'service_type': 'elb',
    'replace_system': True
}

OTC_SERVICES так же в методе load, добавляются в Connection:

for (service_name, service) in OTC_SERVICES.items():
    if service.get('replace_system', False):
        if service['service_type'] in conn._proxies:
            del conn._proxies[service['service_type']]
    sd = _get_descriptor(service_name)
    conn.add_service(sd)

На этом добавление сервиса завершено, мы можем его использовать через OpenStack SDK.

cfg = openstack.config.get_cloud_region(cloud=TEST_CLOUD_NAME)
conn = connection.Connection(config=cfg)
sdk.register_otc_extensions(conn)
cert = conn.elb.create_certificate(
    private_key=PRIVATE_KEY,
    content=CERTIFICATE,
    name=NAME 
)

Ansible collections

Окей, ресурсы теперь есть, осталось разобраться как их использовать, есть отличный вариант, создать коллекцию своих модулей и хранить ее в ansible-galaxy, по аналогии с ansible-collections-openstack создаем коллекцию ansible-collection-cloud, которая основана на OTC extensions.

Если модуль, который мы добавляем в коллекцию существует в OpenStack коллекции, то мы стараемся максимально обеспечить обратную совместимость, создавая единый интерфейс для модулей.

Делаем все по гайду (developing-collections):

ansible-collection-cloud/
    plugins/
        module_utils/
            otc.py
        modules/
            elb_certificate.py
            elb_certificate_info.py

В module_utils, храним базовый для всех модулей класс:

class OTCModule:
    """Openstack Module is a base class for all Openstack Module classes.

    The class has `run` function that should be overriden in child classes,
    the provided methods include:
    """

В нем создается инстанс Connection, и патчится через OTC extensions, чтобы мы могли использовать кастомные ресурсы.

Все модули делятся на два типа с постфиксом _info возвращают информацию о существующем ресурсе, без него создают/изменяют/удаляют ресурсы.

Например, lb_certificate_info:

from ansible_collections.opentelekomcloud.cloud.plugins.module_utils.otc import OTCModule


class LoadBalancerCertificateInfoModule(OTCModule):
    argument_spec = dict(
        name=dict(required=False)
    )

    otce_min_version = '0.10.0'

    def run(self):
        data = []

        if self.params['name']:
            raw = self.conn.elb.find_certificate(name_or_id=self.params['name'], ignore_missing=True)
            if raw:
                dt = raw.to_dict()
                dt.pop('location')
                data.append(dt)
        else:
            for raw in self.conn.elb.certificates():
                dt = raw.to_dict()
                dt.pop('location')
                data.append(dt)

        self.exit_json(
            changed=False,
            elb_certificates=data
        )


def main():
    module = LoadBalancerCertificateInfoModule()
    module()


if __name__ == '__main__':
    main()

аналогично выполнен и lb_certificate.

В настоящий момент идет активная разработка модулей. Сервисы, отмеченные зеленым, полностью покрыты, для остальных в большинстве случаев можно использовать нативные OpenStack модули.

Установка коллекций

Для начала работы необходимо установить коллекции в окружение, для примера используем venv (venv использовать необязательно, но такой подход имеет свои плюсы):

/$ cd ~
~$ python3 -m venv ansiblevenv

Активируем окружение:

~$ source ansiblevenv/bin/activate
(ansiblevenv) ~$

Установим OpenStack Client, otcextensions и wheel (необязательно):

(ansiblevenv) ~$ pip install wheel
(ansiblevenv) ~$ pip install openstackclient
(ansiblevenv) ~$ pip install otcextensions

Для работы с коллекциями далее необходимо установить их из Ansible-Galaxy (Ansible-Galaxy содержит множество свободно распространяемых ролей и коллекций, разрабатываемых сообществом). Дополнительно ставим OpenStack коллекцию для нативных ресурсов:

(ansiblevenv) $ ansible-galaxy collection install opentelekomcloud.cloud
(ansiblevenv) $ ansible-galaxy collection install openstack.cloud

В принципе все для работы с облаком готово, осталось разобраться с авторизацией. OpenStack поддерживает несколько способов авторизации.

Авторизация

clouds.yaml

OpenStack client/sdk самостоятельно ищет файл для авторизации в следующих местах:

  1. system-wide (/etc/openstack/{clouds,secure}.yaml)

  2. Home directory / user space (~/.config/openstack/{clouds,secure}.yaml)

  3. Current directory (./{clouds,secure}.yaml)

clouds:
  otc:
    profile: otc
    auth:
      username: '<USER_NAME>'
      password: '<PASSWORD>'
      project_name: '<eu-de_project>'
      # or project_id: '<123456_PROJECT_ID>'
      user_domain_name: 'OTC00000000001000000xxx'
      # or user_domain_id: '<123456_DOMAIN_ID>'
    account_key: '<AK_VALUE>' # AK/SK pair for access to OBS
    secret_key: '<SK_VALUE>'

После того, как файл создан, указываем переменной окружения OS_CLOUD имя конфигурации, в данном случае:

~$ export OS_CLOUD=otc

Имя может быть любым, и один файл может содержать множество конфигураций.

Чтобы проверить, что все сделано правильно, можно запустить любую команду OpenStack клиента:

~$ openstack server list

Если авторизация успешна, то мы получим список серверов:

Чтобы повысить безопасность, можно вынести чувствительную информацию из clouds.yaml. Рядом с файлом clouds.yaml создаем secure.yaml и помещаем туда все, что хотим скрыть:

clouds:
  otc:
    auth:
      password: '<PASSWORD>'

Переменные окружения

Этот способ заключается в простом использовании переменных окружения, которые можно задавать вручную, либо создать файл, например, .ostackrc:

# .ostackrc file
export OS_USERNAME="<USER_NAME>"
export OS_USER_DOMAIN_NAME=<OTC00000000001000000XYZ>
export OS_PASSWORD=<PASSWORD> # optional
export OS_TENANT_NAME=eu-de
export OS_PROJECT_NAME=<eu-de_PROJECT_NAME>
export OS_AUTH_URL=https://iam.eu-de.otc.t-systems.com:443/v3
export NOVA_ENDPOINT_TYPE=publicURL
export OS_ENDPOINT_TYPE=publicURL
export CINDER_ENDPOINT_TYPE=publicURL
export OS_VOLUME_API_VERSION=2
export OS_IDENTITY_API_VERSION=3
export OS_IMAGE_API_VERSION=2

Создаем переменные:

~$ source .ostackrc

С авторизацией разобрались! Теперь можно полноценно использовать коллекции.

Использование коллекции

Как мы знаем в коллекции два типа модулей: с постфиксом info возвращают информацию о существующем ресурсе, без него создают/изменяют/удаляют ресурсы. Все модули вызываются по полному имени: opentelekom.cloud.*

Все info модули поддерживают поиск как по имени, так и по id ресурса, например:

- name: Get loadbalancer info
  opentelekomcloud.cloud.loadbalancer_info:
    name: "{{ lb_name_or_id }}"
  register: result

Если передано имя ресурса, то в ответе вернется dict с параметрами ресурса, если имя не указано, то появится список всех доступных в проекте ресурсов. Не инфо модули также возвращают dict.

Пример сценария

Для примера использования коллекций создадим файл example.yaml и будем там описывать различные ресурсы. Создадим небольшую инфраструктуру: сеть, сервер и балансировщик нагрузки.

Для создания нативных ресурсов всегда используются OpenStack модули, например, сеть:

---
- name: Create main network
  openstack.cloud.network:
    name: my_network
  register: net

- name: Getting info about external network
  openstack.cloud.networks_info:
    name: admin_external_net
  register: ext_net

- name: Create subnet
  openstack.cloud.subnet:
    name: my_subnet
    network_name: "{{ net.network.name }}"
    cidr: 192.168.0.0/16
    dns_nameservers:
          - 100.125.4.25
          - 100.125.129.199
  register: subnet

- name: Create router
  openstack.cloud.router:
    name: "{{ public_router_scenario }}_router"
    enable_snat: true
    network: "{{ ext_net.openstack_networks[0].id }}"
    interfaces:
      - "{{ subnet.subnet.name }}"
  register: router

Для сервера нам нужен ключ:

- name: Create key pair
  openstack.cloud.keypair:
    name: bastion_key_pair
    public_key_file: "/tmp/keys/public.pub"
  register: keypair

Создадим security group, откроем порты 80, 443 и 22 для ssh, также откроем icmp:

- name: Create security group
  openstack.cloud.security_group:
    name: bastion_secgroup
    description: Allow external connections to ssh, http, https and icmp
  register: sec_group

- name: Add rules for tcp connection to the security group
  openstack.cloud.security_group_rule:
    security_group: "{{ sec_group.secgroup.name }}"
    protocol: tcp
    port_range_min: "{{ item }}"
    port_range_max: "{{ item }}"
    remote_ip_prefix: 0.0.0.0/0
  loop: 
    - 22
    - 80
    - 443

- name: Add a rule for icmp connection to the security group
  openstack.cloud.security_group_rule:
    security_group: "{{ secur_group.secgroup.name }}"
    protocol: icmp
    port_range_min: -1
    port_range_max: -1
    remote_ip_prefix: 0.0.0.0/0

Для подключения сервера к сети необходимо создать порт:

- name: Create a port for a bastion
  openstack.cloud.port:
    name: bastion_port
    network: net.network.id
    security_groups:
      - "{{ sec_group.secgroup.name }}"
     fixed_ips:
       - ip_address: 192.168.200.10
  register: port

Для создания сервера тоже используются нативные модули. Например, создадим bastion (это те хосты, которые принято использовать как jump для доступа в недоступные снаружи сети). Здесь также представлен пример инъекции команд при создании сервера через userdata:

- name: Getting information about a current image
  openstack.cloud.image_info:
    image: Standard_Debian_10_latest
  register: image

- name: Create a new instance
  openstack.cloud.server:
    state: present
    name: bastion
    flavor: s2.medium.2
    key_name: bastion_key_pair
    availability_zone: eu-de-01
    security_groups:
     - "{{ sec_group.secgroup.name }}"
    timeout: 200
    userdata: |
      {%- raw -%}#!/usr/bin/env bash
                 #setup ssh service config
                 file=/etc/ssh/sshd_config
                 cp -p $file $file.old &&
                     while read key other; do
                         case $key in
                         GatewayPorts) other=yes ;;
                         AllowTcpForwarding) other=yes ;;
                         PubkeyAuthentication) other=yes ;;
                         PermitTunnel) other=yes ;;
                         esac
                         echo "$key $other"
                     done <$file.old > $file
                 sudo service sshd restart

                 mkdir -p /etc/sslcerts/live
                 #generate Diffie-Hellman for TLS
                 sudo openssl dhparam -out /etc/sslcerts/live/dhparams.pem 2048
      {% endraw %}
    nics:
      - port-name: "{{ port.port.name }}"
    boot_from_volume: true
    volume_size: 5
    image: "{{ image.openstack_image.id }}"
    terminate_volume: true
    delete_fip: true
    auto_ip: true
  register: bastion

Для динамической регистрации хоста используем add_host:

- name: Register nodes
  add_host:
    name: "{{ bastion.openstack.name }}"
    groups: bastions
    ansible_host: "{{ bastion.openstack.interface_ip }}"
    ansible_ssh_user: linux
    ansible_ssh_private_key_file: "/path/to/key"

После создания сервера можно проверить подключение:

- name: Wait for nodes to be up
  hosts: bastions
  gather_facts: no
  tasks:
    - name: Wait for nodes to be up
      wait_for_connection:
        timeout: 250

Так как одним из самых популярных сервисов является балансировщик нагрузки, то далее приведу пример его создания уже с использованием наших модулей.

После того, как у нас создана сеть и есть хотя бы один сервер, мы можем создать loadbalancer:

- name: Create loadbalancer
  opentelekomcloud.cloud.loadbalancer:
    name: my_elastic_loadbalancer
    state: present
    vip_subnet: "{{ subnet.subet.id }}"
    vip_address: 192.168.200.100
    auto_public_ip: true
  register: loadbalancer

Далее для loadbalancer создаем listener, если протокол https, то сразу можем создать сертификат:

- name: Create listener http
  opentelekomcloud.cloud.lb_listener:
    state: present
    name: my_listener_http
    protocol: http
    protocol_port: 80
    loadbalancer: "{{ loadbalancer.loadbalancer.id }}"
  register: listener_http

- name: Create Server certificate
  opentelekomcloud.cloud.lb_certificate:
    name: my_https_cetificate
    content: "{{ some_https_certificate }}"
    private_key: "{{ some_loadbalancer_https_key }}"
  register: certificate

- name: Create listener https
  opentelekomcloud.cloud.lb_listener:
    state: present
    name: my_listener_https
    protocol: terminated_https
    protocol_port: 443
    loadbalancer: "{{ loadbalancer.loadbalancer.id }}"
    default_tls_container_ref: "{{certificate.elb_certificate.id }}"
  register: listener_https

Чтобы добавить к балансировщику сервер, необходимо создать пул серверов. Для каждого listener создается отдельный пул:

- name: Create lb pool http
  opentelekomcloud.cloud.lb_pool:
    state: present
    name: my_pool_http
    protocol: http
    lb_algorithm: round_robin
    listener: "{{ listener_http.listener.id }}"
  register: lb_pool_http

- name: Create lb pool https
  opentelekomcloud.cloud.lb_pool:
    state: present
    name: my_pool_https
    protocol: http
    lb_algorithm: round_robin
    listener: "{{ listener_https.listener.id }}"
  register: lb_pool_https

Добавляем сервер в пул:

- name: Create members for a http pool in the load balancer
  opentelekomcloud.cloud.lb_member:
    state: present
    name: my_member_http
    pool: "{{ lb_pool_http.server_group.id }}"
    address: 192.168.200.10
    protocol_port: http
    subnet: "{{ subnet.subet.id }}"
  register: members_http

- name: Create members for a https pool in the load balancer
  opentelekomcloud.cloud.lb_member:
    state: present
    name: my_member_https
    pool: "{{ lb_pool_https.server_group.id }}"
    address: 192.168.200.10
    protocol_port: http
    subnet: "{{ subnet.subet.id }}"
  register: members_https

И, наконец, добавим healthmonitor для каждого пула, чтобы наблюдать за статусом хостов:

- name: Enable health check for http members
  opentelekomcloud.cloud.lb_healthmonitor:
    state: present
    name: http_healthcheck
    pool: "{{ lb_pool_http.server_group.id }}"
    delay: 1
    max_retries: 2
    monitor_timeout: 1
    type: http

- name: Enable health check for https members
  opentelekomcloud.cloud.lb_healthmonitor:
    state: present
    name: https_healthcheck
    pool: "{{ lb_pool_https.server_group.id }}"
    delay: 1
    max_retries: 2
    monitor_timeout: 1
    type: http

Если выполнять плейбук с verbosity, то в консоли мы увидим все параметры создаваемых ресурсов.

В результате на консоли можно увидеть наш сервер, балансировщик нагрузки и все остальные ресурсы:

Таким образом мы перевели инфраструктуру наших мониторингов полностью на Ansible.

Насколько мне известно, в России не одна компания пользуется услугами Huawei для создания собственных облачных сервисов, было бы интересно увидеть в комментариях, приходилось ли им решать подобные вопросы касаемо расширения ванильного OpenStack SDK и как они к этому подходили.

Весь код находится в публичном доступе и хранится на Github:

  • Коллекции: ansible-collection-cloud

    • полный список модулей: modules

    • в качестве примеров можно использовать integration tests

  • OTC Extensions: python-otcextensions

  • Ansible galaxy: https://galaxy.ansible.com/opentelekomcloud/cloud

  • Open Telekom Cloud docs: https://docs.otc.t-systems.com/

  • Open Telekom Cloud main page: https://open-telekom-cloud.com/

  • Terraform Provider: terraform-provider-opentelekomcloud

  • Драйвера для Rancher, начиная с версии 2.5 включены в релиз:

    • Open Telekom Cloud CCE cluster driver

    • Open Telekom Cloud node driver

Если тема интересна, то буду рад поделиться своим опытом по работе с другими инструментами. Пишите в комментариях, готов ответить на ваши вопросы!

Источник: https://habr.com/ru/company/deutschetelekomitsolutions/blog/554384/


Интересные статьи

Интересные статьи

Введение Во время работы над задачами машинного обучения с онлайн-данными есть необходимость собирать различные сущности в одну для дальнейшего анализа и оценки. Процесс сбора должен быт...
Начальник заставляет работать по ночам или в выходные? Орёт при всех матом? Позволяет себе пошлые шутки? Коллеги постоянно и настоятельно требуют помощи? Премию так и не дают, а зарпл...
Устраивать конкурсы в инстаграме сейчас модно. И удобно. Инстаграм предоставляет достаточно обширный API, который позволяет делать практически всё, что может сделать обычный пользователь ручками.
На днях столкнулся с задачей примонтировать в OSX 10.14 iPad в качестве внешнего диска, с возможностью осуществлять файловые операции из консоли. В интернете я нашел довольно много инструкций как...
Всем привет. Все кто хоть немного разбирался в теме OpenGL знают, что существует большое количество статей и курсов по этой теме, но многие не затрагивают современный API, а часть из них вооб...