Пример #1
0
    def __init__(self,
                 server,
                 token,
                 jh=None,
                 settings=None,
                 init_projects=False,
                 init_api_resources=False,
                 local=False):
        self.server = server
        oc_base_cmd = [
            'oc', '--kubeconfig', '/dev/null', '--server', server, '--token',
            token
        ]

        if jh is not None:
            self.jump_host = JumpHostSSH(jh, settings=settings)
            oc_base_cmd = self.jump_host.get_ssh_base_cmd() + oc_base_cmd

        self.oc_base_cmd = oc_base_cmd
        # calling get_version to check if cluster is reachable
        if not local:
            self.get_version()
        self.init_projects = init_projects
        if self.init_projects:
            self.projects = [
                p['metadata']['name'] for p in self.get_all('Project')['items']
            ]
        self.init_api_resources = init_api_resources
        if self.init_api_resources:
            self.api_resources = self.get_api_resources()
        else:
            self.api_resources = None
Пример #2
0
    def __init__(self,
                 cluster_name,
                 server,
                 token,
                 jh=None,
                 settings=None,
                 init_projects=False,
                 init_api_resources=False,
                 local=False):
        """Initiates an OC client

        Args:
            cluster_name (string): Name of cluster
            server (string): Server URL of the cluster
            token (string): Token to use for authentication
            jh (dict, optional): Info to initiate JumpHostSSH
            settings (dict, optional): App-interface settings
            init_projects (bool, optional): Initiate projects
            init_api_resources (bool, optional): Initiate api-resources
            local (bool, optional): Use oc locally
        """
        self.cluster_name = cluster_name
        self.server = server
        oc_base_cmd = ['oc', '--kubeconfig', '/dev/null']
        if server:
            oc_base_cmd.extend(['--server', server])

        if token:
            oc_base_cmd.extend(['--token', token])

        self.jump_host = None
        if jh is not None:
            self.jump_host = JumpHostSSH(jh, settings=settings)
            oc_base_cmd = self.jump_host.get_ssh_base_cmd() + oc_base_cmd

        self.oc_base_cmd = oc_base_cmd

        # calling get_version to check if cluster is reachable
        if not local:
            self.get_version()
        self.init_projects = init_projects
        if self.init_projects:
            self.projects = \
                [p['metadata']['name']
                 for p
                 in self.get_all('Project.project.openshift.io')['items']]
        self.init_api_resources = init_api_resources
        if self.init_api_resources:
            self.api_resources = self.get_api_resources()
        else:
            self.api_resources = None

        self.slow_oc_reconcile_threshold = \
            float(os.environ.get('SLOW_OC_RECONCILE_THRESHOLD', 600))

        self.is_log_slow_oc_reconcile = \
            os.environ.get('LOG_SLOW_OC_RECONCILE', '').lower() \
            in ['true', 'yes']
Пример #3
0
 def set_jh_ports(self, jh):
     # This will be replaced with getting the data from app-interface in
     # a future PR.
     jh['remotePort'] = 8888
     key = f"{jh['hostname']}:{jh['remotePort']}"
     with self._lock:
         if key not in self.jh_ports:
             port = JumpHostSSH.get_unique_random_port()
             self.jh_ports[key] = port
         jh['localPort'] = self.jh_ports[key]
Пример #4
0
    def __init__(self,
                 server,
                 token,
                 jh=None,
                 settings=None,
                 init_projects=False,
                 init_api_resources=False,
                 local=False):
        self.server = server
        oc_base_cmd = [
            'oc', '--kubeconfig', '/dev/null', '--server', server, '--token',
            token
        ]

        if jh is not None:
            self.jump_host = JumpHostSSH(jh, settings=settings)
            oc_base_cmd = self.jump_host.get_ssh_base_cmd() + oc_base_cmd

        self.oc_base_cmd = oc_base_cmd
        # calling get_version to check if cluster is reachable
        if not local:
            self.get_version()
        self.init_projects = init_projects
        if self.init_projects:
            self.projects = \
                [p['metadata']['name']
                 for p
                 in self.get_all('Project.project.openshift.io')['items']]
        self.init_api_resources = init_api_resources
        if self.init_api_resources:
            self.api_resources = self.get_api_resources()
        else:
            self.api_resources = None

        self.slow_oc_reconcile_threshold = \
            float(os.environ.get('SLOW_OC_RECONCILE_THRESHOLD', 600))

        self.is_log_slow_oc_reconcile = \
            os.environ.get('LOG_SLOW_OC_RECONCILE', '').lower() \
            in ['true', 'yes']
Пример #5
0
class OC:
    def __init__(self,
                 server,
                 token,
                 jh=None,
                 settings=None,
                 init_projects=False,
                 init_api_resources=False,
                 local=False):
        self.server = server
        oc_base_cmd = [
            'oc', '--kubeconfig', '/dev/null', '--server', server, '--token',
            token
        ]

        if jh is not None:
            self.jump_host = JumpHostSSH(jh, settings=settings)
            oc_base_cmd = self.jump_host.get_ssh_base_cmd() + oc_base_cmd

        self.oc_base_cmd = oc_base_cmd
        # calling get_version to check if cluster is reachable
        if not local:
            self.get_version()
        self.init_projects = init_projects
        if self.init_projects:
            self.projects = \
                [p['metadata']['name']
                 for p
                 in self.get_all('Project.project.openshift.io')['items']]
        self.init_api_resources = init_api_resources
        if self.init_api_resources:
            self.api_resources = self.get_api_resources()
        else:
            self.api_resources = None

        self.slow_oc_reconcile_threshold = \
            float(os.environ.get('SLOW_OC_RECONCILE_THRESHOLD', 600))

        self.is_log_slow_oc_reconcile = \
            os.environ.get('LOG_SLOW_OC_RECONCILE', '').lower() \
            in ['true', 'yes']

    def whoami(self):
        return self._run(['whoami'])

    def cleanup(self):
        if hasattr(self, 'jump_host') and \
                isinstance(self.jump_host, JumpHostSSH):
            self.jump_host.cleanup()

    def get_items(self, kind, **kwargs):
        cmd = ['get', kind, '-o', 'json']

        if 'namespace' in kwargs:
            namespace = kwargs['namespace']
            # for cluster scoped integrations
            # currently only openshift-clusterrolebindings
            if namespace != 'cluster':
                if not self.project_exists(namespace):
                    return []
                cmd.extend(['-n', namespace])

        if 'labels' in kwargs:
            labels_list = [
                "{}={}".format(k, v) for k, v in kwargs.get('labels').items()
            ]

            cmd.append('-l')
            cmd.append(','.join(labels_list))

        resource_names = kwargs.get('resource_names')
        if resource_names:
            items = []
            for resource_name in resource_names:
                resource_cmd = cmd + [resource_name]
                item = self._run_json(resource_cmd, allow_not_found=True)
                if item:
                    items.append(item)
            items_list = {'items': items}
        else:
            items_list = self._run_json(cmd)

        items = items_list.get('items')
        if items is None:
            raise Exception("Expecting items")

        return items

    def get(self, namespace, kind, name=None, allow_not_found=False):
        cmd = ['get', '-o', 'json', kind]
        if name:
            cmd.append(name)
        if namespace is not None:
            cmd.extend(['-n', namespace])
        return self._run_json(cmd, allow_not_found=allow_not_found)

    def get_all(self, kind, all_namespaces=False):
        cmd = ['get', '-o', 'json', kind]
        if all_namespaces:
            cmd.append('--all-namespaces')
        return self._run_json(cmd)

    def process(self, template, parameters={}):
        parameters_to_process = [f"{k}={v}" for k, v in parameters.items()]
        cmd = ['process', '--local', '--ignore-unknown-parameters', '-f', '-'
               ] + parameters_to_process
        result = self._run(cmd, stdin=json.dumps(template, sort_keys=True))
        return json.loads(result)['items']

    def remove_last_applied_configuration(self, namespace, kind, name):
        cmd = [
            'annotate', '-n', namespace, kind, name,
            'kubectl.kubernetes.io/last-applied-configuration-'
        ]
        self._run(cmd)

    def _msg_to_process_reconcile_time(self, namespace, resource):
        return OCProcessReconcileTimeDecoratorMsg(
            namespace=namespace,
            resource=resource,
            server=self.server,
            slow_oc_reconcile_threshold=self.slow_oc_reconcile_threshold,
            is_log_slow_oc_reconcile=self.is_log_slow_oc_reconcile)

    @OCDecorators.process_reconcile_time
    def apply(self, namespace, resource):
        cmd = ['apply', '-n', namespace, '-f', '-']
        self._run(cmd, stdin=resource.toJSON(), apply=True)
        return self._msg_to_process_reconcile_time(namespace, resource.body)

    @OCDecorators.process_reconcile_time
    def create(self, namespace, resource):
        cmd = ['create', '-n', namespace, '-f', '-']
        self._run(cmd, stdin=resource.toJSON(), apply=True)
        return self._msg_to_process_reconcile_time(namespace, resource.body)

    @OCDecorators.process_reconcile_time
    def replace(self, namespace, resource):
        cmd = ['replace', '-n', namespace, '-f', '-']
        self._run(cmd, stdin=resource.toJSON(), apply=True)
        return self._msg_to_process_reconcile_time(namespace, resource.body)

    @OCDecorators.process_reconcile_time
    def delete(self, namespace, kind, name):
        cmd = ['delete', '-n', namespace, kind, name]
        self._run(cmd)
        resource = {'kind': kind, 'metadata': {'name': name}}
        return self._msg_to_process_reconcile_time(namespace, resource)

    def project_exists(self, name):
        if self.init_projects:
            return name in self.projects

        try:
            self.get(None, 'Project.project.openshift.io', name)
        except StatusCodeError as e:
            if 'NotFound' in str(e):
                return False
            else:
                raise e
        return True

    @OCDecorators.process_reconcile_time
    def new_project(self, namespace):
        cmd = ['new-project', namespace]
        try:
            self._run(cmd)
        except StatusCodeError as e:
            if 'AlreadyExists' not in str(e):
                raise e

        # This return will be removed by the last decorator
        resource = {'kind': 'Namespace', 'metadata': {'name': namespace}}
        return self._msg_to_process_reconcile_time(namespace, resource)

    @OCDecorators.process_reconcile_time
    def delete_project(self, namespace):
        cmd = ['delete', 'project', namespace]
        self._run(cmd)

        # This return will be removed by the last decorator
        resource = {'kind': 'Namespace', 'metadata': {'name': namespace}}
        return self._msg_to_process_reconcile_time(namespace, resource)

    def get_group_if_exists(self, name):
        try:
            return self.get(None, 'Group', name)
        except StatusCodeError as e:
            if 'NotFound' in str(e):
                return None
            else:
                raise e

    def create_group(self, group):
        if self.get_group_if_exists(group) is not None:
            return
        cmd = ['adm', 'groups', 'new', group]
        self._run(cmd)

    def release_mirror(self, from_release, to, to_release, dockerconfig):
        with tempfile.NamedTemporaryFile() as fp:
            content = json.dumps(dockerconfig)
            fp.write(content.encode())
            fp.seek(0)

            cmd = [
                'adm', '--registry-config', fp.name, 'release', 'mirror',
                '--from', from_release, '--to', to, '--to-release-image',
                to_release, '--max-per-registry', '1'
            ]

            self._run(cmd)

    def delete_group(self, group):
        cmd = ['delete', 'group', group]
        self._run(cmd)

    def get_users(self):
        return self.get_all('Users')['items']

    def delete_user(self, user_name):
        user = self.get(None, 'User', user_name)
        cmd = ['delete', 'user', user_name]
        self._run(cmd)
        for identity in user['identities']:
            cmd = ['delete', 'identity', identity]
            self._run(cmd)

    def add_user_to_group(self, group, user):
        cmd = ['adm', 'groups', 'add-users', group, user]
        self._run(cmd)

    def del_user_from_group(self, group, user):
        cmd = ['adm', 'groups', 'remove-users', group, user]
        self._run(cmd)

    def sa_get_token(self, namespace, name):
        cmd = ['sa', '-n', namespace, 'get-token', name]
        return self._run(cmd)

    def get_api_resources(self):
        # oc api-resources only has name or wide output
        # and we need to get the KIND, which is the last column
        cmd = ['api-resources', '--no-headers']
        results = self._run(cmd).decode('utf-8').split('\n')
        return [r.split()[-1] for r in results]

    def get_version(self):
        # this is actually a 10 second timeout, because: oc reasons
        cmd = ['version', '--request-timeout=5']
        return self._run(cmd)

    @retry(exceptions=(JobNotRunningError), max_attempts=20)
    def wait_for_job_running(self, namespace, name):
        logging.info('waiting for job to run: ' + name)
        pods = self.get_items('Pod',
                              namespace=namespace,
                              labels={'job-name': name})

        ready_pods = [
            pod for pod in pods
            if pod['status'].get('phase') in ('Running', 'Succeeded')
        ]

        if not ready_pods:
            raise JobNotRunningError(name)

    def job_logs(self, namespace, name, follow, output):
        self.wait_for_job_running(namespace, name)
        cmd = ['logs', '-n', namespace, f'job/{name}']
        if follow:
            cmd.append('-f')
        output_file = open(os.path.join(output, name), 'w')
        # collect logs to file async
        Popen(self.oc_base_cmd + cmd, stdout=output_file)

    @staticmethod
    def get_service_account_username(user):
        namespace = user.split('/')[0]
        name = user.split('/')[1]
        return "system:serviceaccount:{}:{}".format(namespace, name)

    def recycle_pods(self, dry_run, namespace, dep_kind, dep_resource):
        """ recycles pods which are using the specified resources.
        will only act on Secrets containing the 'qontract.recycle' annotation.
        dry_run: simulate pods recycle.
        namespace: namespace in which dependant resource is applied.
        dep_kind: dependant resource kind. currently only supports Secret.
        dep_resource: dependant resource. """

        supported_kinds = ['Secret', 'ConfigMap']
        if dep_kind not in supported_kinds:
            logging.debug(
                ['skipping_pod_recycle_unsupported', namespace, dep_kind])
            return

        dep_annotations = dep_resource.body['metadata'].get('annotations', {})
        qontract_recycle = dep_annotations.get('qontract.recycle')
        if qontract_recycle is True:
            raise RecyclePodsInvalidAnnotationValue('should be "true"')
        if qontract_recycle != 'true':
            logging.debug(
                ['skipping_pod_recycle_no_annotation', namespace, dep_kind])
            return

        dep_name = dep_resource.name
        pods = self.get(namespace, 'Pods')['items']

        if dep_kind == 'Secret':
            pods_to_recycle = [
                pod for pod in pods if self.secret_used_in_pod(dep_name, pod)
            ]
        elif dep_kind == 'ConfigMap':
            pods_to_recycle = [
                pod for pod in pods
                if self.configmap_used_in_pod(dep_name, pod)
            ]
        else:
            raise RecyclePodsUnsupportedKindError(dep_kind)

        recyclables = {}
        supported_recyclables = [
            'Deployment',
            'DeploymentConfig',
            'StatefulSet',
        ]
        for pod in pods_to_recycle:
            owner = self.get_obj_root_owner(namespace, pod)
            kind = owner['kind']
            if kind not in supported_recyclables:
                continue
            recyclables.setdefault(kind, [])
            exists = False
            for obj in recyclables[kind]:
                owner_name = owner['metadata']['name']
                if obj['metadata']['name'] == owner_name:
                    exists = True
                    break
            if not exists:
                recyclables[kind].append(owner)

        for kind, objs in recyclables.items():
            for obj in objs:
                name = obj['metadata']['name']
                logging.info([f'recycle_{kind.lower()}', namespace, name])
                if not dry_run:
                    now = datetime.now()
                    recycle_time = now.strftime("%d/%m/%Y %H:%M:%S")

                    # honor update strategy by setting annotations to force
                    # a new rollout
                    a = obj['spec']['template']['metadata'].get(
                        'annotations', {})
                    a['recycle.time'] = recycle_time
                    obj['spec']['template']['metadata']['annotations'] = a
                    cmd = ['apply', '-n', namespace, '-f', '-']
                    stdin = json.dumps(obj, sort_keys=True)
                    self._run(cmd, stdin=stdin, apply=True)

    def get_obj_root_owner(self, ns, obj):
        refs = obj['metadata'].get('ownerReferences', [])
        for r in refs:
            if r.get('controller'):
                controller_obj = self.get(ns, r['kind'], r['name'])
                return self.get_obj_root_owner(ns, controller_obj)
        return obj

    @staticmethod
    def secret_used_in_pod(name, pod):
        volumes = pod['spec']['volumes']
        for v in volumes:
            volume_item = v.get('secret', {})
            try:
                if volume_item['secretName'] == name:
                    return True
            except KeyError:
                continue
        containers = pod['spec']['containers']
        for c in containers:
            for e in c.get('envFrom', []):
                try:
                    if e['secretRef']['name'] == name:
                        return True
                except KeyError:
                    continue
            for e in c.get('env', []):
                try:
                    if e['valueFrom']['secretKeyRef']['name'] == name:
                        return True
                except KeyError:
                    continue
        return False

    @staticmethod
    def configmap_used_in_pod(name, pod):
        volumes = pod['spec']['volumes']
        for v in volumes:
            volume_item = v.get('configMap', {})
            try:
                if volume_item['name'] == name:
                    return True
            except KeyError:
                continue
        containers = pod['spec']['containers']
        for c in containers:
            for e in c.get('envFrom', []):
                try:
                    if e['configMapRef']['name'] == name:
                        return True
                except KeyError:
                    continue
            for e in c.get('env', []):
                try:
                    if e['valueFrom']['configMapKeyRef']['name'] == name:
                        return True
                except KeyError:
                    continue
        return False

    @retry(exceptions=(StatusCodeError, NoOutputError), max_attempts=10)
    def _run(self, cmd, **kwargs):
        if kwargs.get('stdin'):
            stdin = PIPE
            stdin_text = kwargs.get('stdin').encode()
        else:
            stdin = None
            stdin_text = None

        p = Popen(self.oc_base_cmd + cmd,
                  stdin=stdin,
                  stdout=PIPE,
                  stderr=PIPE)
        out, err = p.communicate(stdin_text)

        code = p.returncode

        allow_not_found = kwargs.get('allow_not_found')

        if code != 0:
            err = err.decode('utf-8')
            if kwargs.get('apply'):
                if 'Invalid value: 0x0' in err:
                    raise InvalidValueApplyError(f"[{self.server}]: {err}")
                if 'Invalid value: ' in err:
                    if ': field is immutable' in err:
                        raise FieldIsImmutableError(f"[{self.server}]: {err}")
                    if ': may not change once set' in err:
                        raise MayNotChangeOnceSetError(
                            f"[{self.server}]: {err}")
                    if ': primary clusterIP can not be unset' in err:
                        raise PrimaryClusterIPCanNotBeUnsetError(
                            f"[{self.server}]: {err}")
                if 'metadata.annotations: Too long' in err:
                    raise MetaDataAnnotationsTooLongApplyError(
                        f"[{self.server}]: {err}")
                if 'UnsupportedMediaType' in err:
                    raise UnsupportedMediaTypeError(f"[{self.server}]: {err}")
            if not (allow_not_found and 'NotFound' in err):
                raise StatusCodeError(f"[{self.server}]: {err}")

        if not out:
            if allow_not_found:
                return '{}'
            else:
                raise NoOutputError(err)

        return out.strip()

    def _run_json(self, cmd, allow_not_found=False):
        out = self._run(cmd, allow_not_found=allow_not_found)

        try:
            out_json = json.loads(out)
        except ValueError as e:
            raise JSONParsingError(out + "\n" + str(e))

        return out_json