Пример #1
0
class Resource(resource.Resource):
    """Workloads API methods"""
    @command(parser_options=dict(description=LIST_DESCRIPTION))
    @display_table(Workload)
    def list(self):
        """
        Get list of Lava workloads

        :returns: List of :class:`~lavaclient.api.response.Workload` objects
        """
        return self._parse_response(self._client._get('/workloads'),
                                    WorkloadsResponse,
                                    wrapper='workloads')

    @command(parser_options=dict(
        description='Get recommendations for a certain workload',
        epilog=RECOMMENDATIONS_EPILOG,
    ),
             storage_size=argument(type=int,
                                   help='Required disk space (in GB)'),
             persistence=argument(choices=['all', 'data', 'none'],
                                  help='See PERSISTENCE'))
    @display(display_recommendations)
    def recommendations(self, workload_id, storage_size, persistence):
        """
        Get recommendations for the given workload.

        :param workload_id: Workload ID (see the :func:`get` method)
        :param storage_size: The desired amount of disk space (in GB)
        :param persistence: One of the following values:
            - 'data': persistent (large) data storage is required
            - 'all': persist data and your cluster servers
            - 'none': your data and clusters are ephemeral
        :returns: List of :class:`~lavaclient.api.response.Recommendations`
                  objects
        """
        params = self._marshal_request(
            {
                'storagesize': storage_size,
                'persistent': persistence
            }, RecommendationParams)
        return self._parse_response(self._client._get(
            '/workloads/{0}/recommendations'.format(workload_id),
            params=params),
                                    RecommendationsResponse,
                                    wrapper='recommendations')
Пример #2
0
class Resource(resource.Resource):
    """
    Clusters API methods
    """
    @command(
        parser_options=dict(description='List all existing clusters', ), )
    @display_table(Cluster)
    def list(self):
        """
        List clusters that belong to the tenant specified in the client

        :returns: List of :class:`~lavaclient.api.response.Cluster` objects
        """
        return self._parse_response(self._client._get('clusters'),
                                    ClustersResponse,
                                    wrapper='clusters')

    @command(
        parser_options=dict(
            description='Display an existing cluster in detail', ), )
    @display_table(ClusterDetail)
    def get(self, cluster_id):
        """
        Get the cluster corresponding to the cluster ID

        :param cluster_id: Cluster ID
        :returns: :class:`~lavaclient.api.response.ClusterDetail`
        """
        return self._parse_response(
            self._client._get('clusters/' + six.text_type(cluster_id)),
            ClusterResponse,
            wrapper='cluster')

    def _gather_node_groups(self, node_groups):
        """Transform node_groups into a list of dicts"""
        if isinstance(node_groups, dict):
            node_group_list = []
            for key, node_group in six.iteritems(node_groups):
                node_group.update(id=key)
                node_group_list.append(node_group)

            return node_group_list
        else:
            return node_groups

    def create(self,
               name,
               stack_id,
               username=None,
               ssh_keys=None,
               user_scripts=None,
               node_groups=None,
               connectors=None,
               wait=False,
               credentials=None):
        """
        Create a cluster

        :param name: Cluster name
        :param stack_id: Valid stack identifier
        :param username: User to create on the cluster; defaults to local user
        :param ssh_keys: List of SSH keys; if none is specified, it will use
                         the key `user@hostname`, creating the key from
                         `$HOME/.ssh/id_rsa.pub` if it doesn't exist.
        :param node_groups: `dict` of `(node_group_id, attrs)` pairs, in which
                            `attrs` is a `dict` of node group attributes.
                            Instead of a `dict`, you may give a `list` of
                            `dicts`, each containing the `id` key. Currently
                            supported attributes are `flavor_id` and `count`
        :param user_scripts: List of user script ID's; See
                             :meth:`lavaclient.api.scripts.Resource.create`
        :param credentials: List of credentials to use. Each item must be a
                            dictionary of `(type, name)` pairs
        :param connectors: List of connector credentials to use. Each item
                           must be a dictionary of `(type, name)` pairs.
                           Deprecated in favor of `credentials`
        :param wait: If `True`, wait for the cluster to become active before
                     returning
        :returns: :class:`~lavaclient.api.response.ClusterDetail`
        """
        if ssh_keys is None:
            ssh_keys = [DEFAULT_SSH_KEY]
        if username is None:
            username = getuser()
        if connectors is None:
            connectors = []
        if credentials is None:
            credentials = []

        if connectors:
            deprecation('connectors are deprecated; use credentials')

        data = dict(name=name,
                    username=username,
                    ssh_keys=ssh_keys,
                    stack_id=stack_id)

        if node_groups:
            data.update(node_groups=self._gather_node_groups(node_groups))

        if user_scripts:
            data.update(scripts=[{'id': script} for script in user_scripts])

        if connectors or credentials:
            cdata = []
            for credential in connectors + credentials:
                ctype, name = six.next(six.iteritems(credential))
                cdata.append({'type': ctype, 'credential': {'name': name}})
            data.update(credentials=cdata)

        request_data = self._marshal_request(data,
                                             ClusterCreateRequest,
                                             wrapper='cluster')

        cluster = self._parse_response(self._client._post('clusters',
                                                          json=request_data),
                                       ClusterResponse,
                                       wrapper='cluster')

        if wait:
            return self.wait(cluster.id)

        return cluster

    @command(
        parser_options=dict(description='Resize an existing Lava cluster', ),
        node_groups=argument(
            type=parse_node_group,
            action='append',
            help='Node group options; may be used multiple times to resize '
            'multiple node groups. Each option should be in the form '
            '\'<id>(count=<value>)\', where <id> is a valid node '
            'group ID for the cluster and the count is the '
            'option to specify new count for that node group. '),
        wait=argument(action='store_true',
                      help='Wait for the cluster to become active'),
    )
    @display_table(ClusterDetail)
    def resize(self, cluster_id, node_groups=None, wait=False):
        """
        Resize a cluster

        :param cluster_id: Cluster ID
        :param node_groups: `dict` of `(node_group_id, attrs)` pairs, in which
                            `attrs` is a `dict` of node group attributes.
                            Instead of a `dict`, you may give a `list` of
                            `dicts`, each containing the `id` key. Currently
                            supported attributes are `flavor_id` and `count`
        :returns: :class:`~lavaclient.api.response.ClusterDetail`
        """
        if not node_groups:
            raise error.RequestError("Must specify atleast one node_group "
                                     "to resize")

        gathered = self._gather_node_groups(node_groups)
        if not all('count' in node_group and 'id' in node_group
                   for node_group in gathered):
            raise error.RequestError("Invalid or missing option "
                                     "in the node groups")

        data = dict(cluster=dict(node_groups=gathered))

        request_data = self._marshal_request(data, ClusterUpdateRequest)

        cluster = self._parse_response(self._client._put(
            'clusters/{0}'.format(cluster_id), json=request_data),
                                       ClusterResponse,
                                       wrapper='cluster')

        if wait:
            return self.wait(cluster.id)

        return cluster

    @command(
        parser_options=dict(
            description='Update the connector credentials for a cluster', ),
        credentials=argument(
            '--credential',
            type=parse_credential,
            action='append',
            help='Credentials to use in the cluster. Only the credentials you '
            'specify are modified. No credentials are removed. Each must be'
            ' in the form of `type=name`. See `lava credentials`'),
        wait=argument(action='store_true',
                      help='Wait for the cluster to become active'))
    @display_table(ClusterDetail)
    def _update_credentials(self, cluster_id, credentials=None, wait=False):
        if not credentials:
            raise error.RequestError("Must specify at least one credential")

        creds = defaultdict(list)
        for cred in credentials:
            ctype, name = six.next(c for c in six.iteritems(cred))
            creds[ctype].append(name)
        return self.update_credentials(cluster_id, creds, wait)

    def update_credentials(self, cluster_id, credentials=None, wait=False):
        """
        Update the credentials on a cluster

        :param cluster_id: Cluster ID
        :param credentials: The credential name to on update the cluster.
                            Credentials must be a dict of {type: [name]} values
        :param wait: If `True`, wait for the cluster to become active before
                     returning
        :returns: :class:`~lavaclient.api.response.ClusterDetail`
        """
        new_creds = []
        for ctype in credentials:
            names = credentials[ctype]
            for name in names:
                new_creds.append({'name': name, 'type': ctype})

        request_data = self._marshal_request({'credentials': new_creds},
                                             ClusterCredentialsRequest,
                                             wrapper='cluster')

        response = self._client._put('clusters/{0}'.format(cluster_id),
                                     json=request_data)
        cluster = self._parse_response(response,
                                       ClusterResponse,
                                       wrapper='cluster')

        if wait:
            return self.wait(cluster.id)

        return cluster

    @command(parser_options=dict(
        description='Remove the specified ssh keys from a cluster cluster', ),
             keynames=argument(
                 metavar='keyname',
                 nargs='+',
                 help='Name of ssh credential to remove from the cluster'),
             wait=argument(action='store_true',
                           help='Wait for the cluster to become active'),
             force=argument(action='store_true',
                            help="Suppress delete confirmation dialog"))
    @display_table(ClusterDetail)
    def _delete_ssh_credentials(self,
                                cluster_id,
                                keynames,
                                wait=False,
                                force=False):
        if not force:
            if not confirm('Remove these keys from this cluster?'):
                return

        return self.delete_ssh_credentials(cluster_id, keynames, wait)

    def delete_ssh_credentials(self, cluster_id, keynames, wait):
        """
        Remove the specified SSH credentials from a cluster.

        :param cluster_id: Cluster ID
        :param keys: The ssh key names to remove from the cluster
        :param wait: If `True`, wait for the cluster to become active before
                     returning
        :returns: :class:`~lavaclient.api.response.ClusterDetail`
        """

        creds = []

        for name in keynames:
            creds.append({'name': name, 'type': 'ssh_keys'})

        request_data = self._marshal_request({'remove_credentials': creds},
                                             ClusterCredentialsRemovalRequest,
                                             wrapper='cluster')
        response = self._client._put('clusters/{0}'.format(cluster_id),
                                     json=request_data)
        cluster = self._parse_response(response,
                                       ClusterResponse,
                                       wrapper='cluster')

        if wait:
            return self.wait(cluster.id)

        return cluster

    def _create_default_ssh_credential(self):
        if not confirm(
                'You have not uploaded any SSH key credentials; do '
                'you want to upload {0} now?'.format(DEFAULT_SSH_PUBKEY)):
            sys.exit(1)

        try:
            with open(expand(DEFAULT_SSH_PUBKEY)) as f:
                six.print_('SSH key does not exist; creating...')
                self._client.credentials.create_ssh_key(
                    DEFAULT_SSH_KEY,
                    f.read().strip())
        except IOError:
            six.print_('No SSH keypair found; to generate a keypair, run '
                       '`ssh-keygen`')
            sys.exit(1)

        return DEFAULT_SSH_KEY

    @command(
        parser_options=dict(description='Create a new Lava cluster', ),
        name=argument(help='Cluster name'),
        username=argument(
            help='Login name of the user to install onto the created nodes; '
            'defaults to {0}'.format(getuser()),
            default=getuser()),
        ssh_keys=argument(
            '--ssh-key',
            action='append',
            help="SSH key name; may be used multiple times. If not "
            "specified, the client will attempt to use the key "
            "'{key}', creating it from ~/.ssh/id_rsa.pub if it "
            "doesn't exist. See `lava credentials`".format(
                key=DEFAULT_SSH_KEY)),
        stack_id=argument(
            help='Valid Lava stack ID. For a list of stacks, use the '
            '`lava stacks list` command'),
        user_scripts=argument(
            '--user-script',
            action='append',
            help='User script ID: See `lava scripts --help` for more '
            'information; may be used multiple times to provide multiple '
            'script IDs'),
        node_groups=argument(
            type=parse_node_group,
            action='append',
            help='Node group options; may be used multiple times to '
            'configure multiple node groups. Each option should be in '
            'the form <id>(<key>=<value>, ...), where <id> is a valid '
            'node group ID for the stack and the key-value pairs are '
            'options to specify for that node group. Current valid '
            'options are `count` and `flavor_id`'),
        credentials=argument(
            '--credential',
            action='append',
            type=parse_credential,
            help='Credentials to use in the cluster. Each must be in the '
            'form of `type=name`. See `lava credentials`'),
        connectors=argument('--connector',
                            action='append',
                            type=parse_credential,
                            help='Deprecated'),
        wait=argument(action='store_true',
                      help='Wait for the cluster to become active'),
    )
    @display_table(ClusterDetail)
    def _create(self,
                name,
                stack_id,
                username=None,
                ssh_keys=None,
                user_scripts=None,
                node_groups=None,
                connectors=None,
                wait=False,
                credentials=None):
        """
        CLI-only; cluster create command
        """
        if ssh_keys is None:
            ssh_keys = [DEFAULT_SSH_KEY]

        try:
            return self.create(name,
                               stack_id,
                               username=username,
                               ssh_keys=ssh_keys,
                               user_scripts=user_scripts,
                               node_groups=node_groups,
                               connectors=connectors,
                               wait=wait,
                               credentials=credentials)
        except error.RequestError as exc:
            if self._args.headless or not (
                    ssh_keys == [DEFAULT_SSH_KEY] and
                ('Cannot find requested ssh_keys' in str(exc)
                 or 'One or more ssh_keys are invalid' in str(exc))):
                raise

        # Create the SSH key for the user and then attempt to create the
        # cluster again
        self._create_default_ssh_credential()

        return self.create(name,
                           stack_id,
                           username=username,
                           ssh_keys=ssh_keys,
                           user_scripts=user_scripts,
                           node_groups=node_groups,
                           connectors=connectors,
                           wait=wait,
                           credentials=credentials)

    @command(
        parser_options=dict(description='Delete a cluster', ),
        force=argument(action='store_true',
                       help="Suppress delete confirmation dialog"),
    )
    def _delete(self, cluster_id, force=False):
        if not force:
            display_result(self.get(cluster_id), ClusterDetail)

            if not confirm('Delete this cluster?'):
                return

        self.delete(cluster_id)

    def delete(self, cluster_id):
        """
        Delete a cluster

        :param cluster_id: Cluster ID
        :returns: :class:`~lavaclient.api.response.ClusterDetail`
        """
        self._client._delete('clusters/' + six.text_type(cluster_id))

    @coroutine
    def _cli_wait_printer(self, start):
        """Coroutine that runs during the wait command. Prints status to stdout
        if the command line is running; otherwise, just log the status."""

        started = False

        while True:
            cluster = yield
            LOG.debug('Cluster {0}: {1}'.format(cluster.id, cluster.status))

            if self._command_line:
                if not started:
                    started = True
                    cli_msg_length = 0
                    six.print_('Waiting for cluster {0}'.format(cluster.id))

                msg = 'Status: {0} (Elapsed time: {1:.1f} minutes)'.format(
                    cluster.status, elapsed_minutes(start))
                sys.stdout.write('\b' * cli_msg_length + msg)
                sys.stdout.flush()
                cli_msg_length = len(msg)

                if cluster.status == 'ACTIVE':
                    six.print_('\n')

    @command(
        parser_options=dict(
            description='Poll a cluster until it becomes active'),
        timeout=argument(type=natural_number,
                         help='Poll timeout (in minutes)'),
        interval=argument(type=natural_number,
                          help='Poll interval (in seconds)'),
    )
    @display_table(ClusterDetail)
    def wait(self, cluster_id, timeout=None, interval=None):
        """
        Wait (blocking) for a cluster to either become active or fail.

        :param cluster_id: Cluster ID
        :param timeout: Wait timeout in minutes (default: no timeout)
        :returns: :class:`~lavaclient.api.response.ClusterDetail`
        """
        if interval is None:
            interval = WAIT_INTERVAL

        interval = max(MIN_INTERVAL, interval)

        delta = timedelta(minutes=timeout) if timeout else timedelta(days=365)

        start = datetime.now()
        timeout_date = start + delta

        printer = self._cli_wait_printer(start)

        while datetime.now() < timeout_date:
            cluster = self.get(cluster_id)
            printer.send(cluster)

            if cluster.status == 'ACTIVE':
                return cluster
            elif cluster.status not in IN_PROGRESS_STATES:
                raise error.FailedError('Cluster status is {0}'.format(
                    cluster.status))

            if datetime.now() + timedelta(seconds=interval) >= timeout_date:
                break

            time.sleep(interval)

        raise error.TimeoutError(
            'Cluster did not become active before timeout')

    @command(parser_options=dict(description='List all nodes in the cluster'))
    @display(Node.display_nodes)
    def nodes(self, cluster_id):
        """
        Get the cluster nodes

        :param cluster_id: Cluster ID
        :returns: List of :class:`~lavaclient.api.response.Node` objects
        """
        return self._client.nodes.list(cluster_id)

    def _get_named_node(self, nodes, node_name=None, wait=False):
        if node_name is None:
            return nodes[0]

        try:
            return six.next(node for node in nodes
                            if node.name.lower() == node_name.lower())
        except StopIteration:
            raise error.InvalidError(
                'Invalid node: {0}; available nodes are {1}'.format(
                    node_name, ', '.join(node.name for node in nodes)))

    def _get_component_node(self, nodes, component, wait=False):
        components_by_node = ((node, (comp['name'].lower()
                                      for comp in node.components))
                              for node in nodes)

        try:
            return six.next(node for node, components in components_by_node
                            if component.lower() in components)
        except StopIteration:
            raise error.InvalidError(
                'Component {0} not found in cluster'.format(component))

    def _ssh_cluster_nodes(self, cluster_id, wait=False):
        """
        Return `(cluster, nodes)`, where `nodes` is a list of all non-Ambari
        nodes in the cluster. If the cluster is not ACTIVE/ERROR/IMPAIRED and
        wait is `True`, the function will block until it becomes active;
        otherwise, an exception is thrown.
        """
        cluster = self.get(cluster_id)
        status = cluster.status.upper()
        if status not in FINAL_STATES:
            LOG.debug('Cluster status: %s', status)

            if status not in IN_PROGRESS_STATES:
                raise error.InvalidError(
                    'Cluster is in state {0}'.format(status))
            elif not wait:
                raise error.InvalidError('Cluster is not yet active')

            self.wait(cluster_id)

        if cluster.status.upper() == 'ERROR':
            raise error.InvalidError('Cluster is in state ERROR')

        nodes = [
            node for node in self.nodes(cluster_id)
            if node.name.lower() != 'ambari'
        ]
        return cluster, nodes

    @command(
        parser_options=dict(
            description='Create a SOCKS5 proxy over SSH to a node in the '
            'cluster'),
        port=argument(type=int,
                      help='Port on localhost on which to create '
                      'the proxy'),
        node_name=argument(help='Name of node on which to make the SSH '
                           'connection, e.g. gateway-1. By default, use '
                           'first available node.'),
        ssh_command=argument(help="SSH command (default: 'ssh')"),
        wait=argument(action='store_true',
                      help="Wait for cluster to become active (if it isn't "
                      "already)"),
    )
    def ssh_proxy(self,
                  cluster_id,
                  port=None,
                  node_name=None,
                  ssh_command=None,
                  wait=False):
        """
        Set up a SOCKS5 proxy over SSH to a node in the cluster. Returns the
        SSH process (via :py:class:`~subprocess.Popen`), which can be
        stopped via the :func:`kill` method.

        :param cluster_id: Cluster ID
        :param port: Local port on which to create the proxy (default: 12345)
        :param node_name: Name of node on which to make the SSH connection. By
                          default, use first available node.
        :param wait: If `True`, wait for the cluster to become active before
                     creating the proxy
        :returns: :py:class:`~subprocess.Popen` object representing the SSH
                  connection.
        """
        if port is None:
            port = 12345

        cluster, nodes = self._ssh_cluster_nodes(cluster_id, wait=wait)
        ssh_node = self._get_named_node(nodes, node_name=node_name)

        # Get a URL to test the proxy against
        all_urls = itertools.chain.from_iterable([
            component['uri'] for component in node.components
            if 'uri' in component and component['uri'].startswith('http')
        ] for node in nodes)
        test_url = six.next(all_urls, None)

        printer = self._cli_printer(LOG)
        printer('Starting SOCKS proxy via node {0} ({1})'.format(
            ssh_node.name, ssh_node.public_ip))

        process = create_socks_proxy(cluster.username,
                                     ssh_node.public_ip,
                                     port,
                                     ssh_command=ssh_command,
                                     test_url=test_url)

        printer(
            'Successfully created SOCKS proxy on localhost:{0}'.format(port),
            logging.INFO)

        if not self._command_line:
            return process

        try:
            printer('Use Ctrl-C to stop proxy', logging.NOTSET)
            process.communicate()
        except KeyboardInterrupt:
            printer('SOCKS proxy closed')

    @command(parser_options=dict(
        description='SSH to a node in the cluster and optionally execute '
        'a command'),
             node_name=argument(help='Name of node on which to make the SSH '
                                'connection, e.g. gateway-1. By default, use '
                                'first available node.'),
             ssh_command=argument(help="SSH command (default: 'ssh')"),
             wait=argument(
                 action='store_true',
                 help="Wait for cluster to become active (if it isn't "
                 "already)"),
             command=argument(help='Command to execute over SSH'))
    def _ssh(self,
             cluster_id,
             node_name=None,
             ssh_command=None,
             wait=False,
             command=None):
        """
        Command-line only. SSH to the desired cluster node.
        """
        output = self._execute_ssh(cluster_id,
                                   node_name=node_name,
                                   ssh_command=ssh_command,
                                   wait=wait,
                                   command=command)
        if command:
            six.print_(output)

    def _execute_ssh(self,
                     cluster_id,
                     node_name=None,
                     ssh_command=None,
                     wait=False,
                     command=None):
        cluster, nodes = self._ssh_cluster_nodes(cluster_id, wait=wait)
        node = self._get_named_node(nodes, node_name=node_name)

        return node._ssh(cluster.username,
                         command=command,
                         ssh_command=ssh_command)

    def ssh_execute(self,
                    cluster_id,
                    node_name,
                    command,
                    ssh_command=None,
                    wait=False):
        """
        Execute a command over SSH to the specified node in the cluster.

        :param cluster_id: Cluster ID
        :param node_name: Name of node on which to make the SSH connection. By
                          default, use first available node.
        :param command: Shell command to execute remotely
        :param ssh_command: SSH shell command to execute locally
                            (default: `'ssh'`)
        :param wait: If `True`, wait for the cluster to become active before
                     creating the proxy
        :returns: The output of running the command
        """
        return self._execute_ssh(cluster_id,
                                 node_name=node_name,
                                 ssh_command=ssh_command,
                                 command=command,
                                 wait=wait)

    @command(
        parser_options=dict(description='Create SSH tunnel'),
        local_port=argument(type=int,
                            help='Port to which to bind on localhost'),
        remote_port=argument(type=int,
                             help='Port to which to bind on cluster node'),
        node_name=argument(help='Name of node on which to make the SSH '
                           'connection, e.g. gateway-1. Mutually '
                           'exclusive with --component'),
        component=argument(help='Find and connect to node containing a '
                           'particular component, e.g. HiveServer2. '
                           'Mutually exclusive with --node-name'),
        ssh_command=argument(help="SSH command (default: 'ssh')"),
        wait=argument(action='store_true',
                      help="Wait for cluster to become active (if it isn't "
                      "already)"),
    )
    def ssh_tunnel(self,
                   cluster_id,
                   local_port,
                   remote_port,
                   node_name=None,
                   component=None,
                   ssh_command=None,
                   wait=False):
        """
        Create a SSH tunnel from the local host to a particular port on a
        cluster node.  Returns the SSH process (via
        :py:class:`~subprocess.Popen`), which can be stopped via the
        :func:`kill` method.

        :param cluster_id: Cluster ID
        :param local_port: Port on which to bind on localhost
        :param remote_port: Port on which to bind on the cluster node
        :param node_name: Name of node on which to make the SSH connection.
                          Mutually exclusive with `component`
        :param component: Name of a component installed on a node in the
                          cluster, e.g. `HiveServer2`. SSH tunnel will be set
                          up on the first node containing the component.
                          Mutually exclusive with `node_name`
        :param wait: If `True`, wait for the cluster to become active before
                     creating the proxy
        :returns: :py:class:`~subprocess.Popen` object representing the SSH
                  connection.
        """
        if node_name and component:
            raise error.InvalidError(
                'node_name and component are mutually exclusive')
        elif not (node_name or component):
            raise error.InvalidError(
                'One of node_name or component is required')

        cluster, nodes = self._ssh_cluster_nodes(cluster_id, wait=wait)

        if component:
            ssh_node = self._get_component_node(nodes, component)
        else:
            ssh_node = self._get_named_node(nodes, node_name=node_name)

        printer = self._cli_printer(LOG)
        printer('Starting SSH tunnel from localhost:{0} to {1}:{2} '
                '({3})'.format(local_port, ssh_node.name, remote_port,
                               ssh_node.public_ip))

        process = create_ssh_tunnel(cluster.username,
                                    ssh_node,
                                    local_port,
                                    remote_port,
                                    ssh_command=ssh_command)

        printer(
            'Successfully created SSH tunnel to localhost:{0}'.format(
                local_port), logging.INFO)

        if not self._command_line:
            return process

        try:
            printer('Use Ctrl-C to stop tunnel', logging.NOTSET)
            process.communicate()
        except KeyboardInterrupt:
            printer('SSH tunnel closed')

    def services(self, action, cluster_id, service_names):
        data = {
            'cluster': {
                'control_services': {
                    'action': action,
                    'services': [{
                        'name': name
                    } for name in service_names],
                }
            }
        }
        return self._parse_response(self._client._put(
            'clusters/{0}'.format(cluster_id), json=data),
                                    ClusterResponse,
                                    wrapper='cluster')

    @command(
        parser_options=dict(
            description='Start/stop/restart services on a cluster'),
        action=argument(choices=['start', 'stop', 'restart']),
        service_names=argument(metavar='<service>',
                               nargs='*',
                               help='Cluster service name, e.g. HDFS'),
        force=argument(action='store_true',
                       help="Don't display confirmation dialog"),
    )
    @display_table(ClusterDetail)
    def _services(self, action, cluster_id, service_names, force=False):
        if not force:
            if service_names:
                services_str = 'service{0} {1}'.format(
                    's' if len(service_names) > 1 else '',
                    ', '.join(service_names))
            else:
                services_str = 'all services'

            dialog = '{0} {1} on cluster {2}?'.format(action.capitalize(),
                                                      services_str, cluster_id)

            if not confirm(dialog):
                return

        return self.services(action, cluster_id, service_names)
Пример #3
0
class Resource(resource.Resource):
    """Flavors API methods"""
    @command(parser_options=dict(description='List all existing stacks', ))
    @display(display_stacks)
    def list(self):
        """
        List all stacks

        :returns: List of :class:`~lavaclient.api.response.Stack` objects
        """
        return self._parse_response(self._client._get('stacks'),
                                    StacksResponse,
                                    wrapper='stacks')

    @command(parser_options=dict(
        description='Show a specific stack in detail', ))
    @display_table(StackDetail)
    def get(self, stack_id):
        """
        Get a specific stack

        :param stack_id: Stack ID
        :returns: :class:`~lavaclient.api.response.StackDetail`
        """
        return self._parse_response(self._client._get(
            'stacks/{0}'.format(stack_id)),
                                    StackResponse,
                                    wrapper='stack')

    @command(parser_options=dict(
        description='Create a custom stack',
        epilog=SERVICE_CREATE_EPILOG,
    ),
             name=argument(help='A stack identifier, e.g. MY_HADOOP_STACK'),
             distro=argument(help='An existing distribution ID; see '
                             '`lava distros list`'),
             services=argument(
                 type=read_json,
                 help='JSON data string or path to file containing '
                 'JSON data; see SERVICES'),
             node_groups=argument(type=read_json,
                                  help='JSON data string or p ath to file '
                                  'containing JSON data; see NODE GROUPS'),
             description=argument(help='A brief description of the purpose '
                                  'of the stack'))
    @display_table(StackDetail)
    def create(self,
               name,
               distro,
               services,
               node_groups=None,
               description=None):
        """
        Create a stack

        :param name: Stack name
        :param distro: Valid distro identifier
        :param services: List of services. Each should have a name and
                         optionally a list of modes
        :param node_groups: List of node groups for the cluster
        :returns: :class:`~lavaclient.api.response.StackDetail`
        """
        data = dict(
            name=name,
            distro=distro,
            services=services,
        )
        if node_groups:
            data.update(node_groups=node_groups)
        if description:
            data.update(description=description)

        request_data = self._marshal_request(data,
                                             CreateStackRequest,
                                             wrapper='stack')

        return self._parse_response(self._client._post('stacks',
                                                       json=request_data),
                                    StackResponse,
                                    wrapper='stack')

    @command(
        parser_options=dict(description='Delete a custom stack'),
        force=argument(action='store_true',
                       help='Do not show confirmation dialog'),
    )
    def _delete(self, stack_id, force=False):
        if not force:
            display_result(self.get(stack_id), StackDetail)

            if not confirm('Delete this stack?'):
                return

        self.delete(stack_id)

    def delete(self, stack_id):
        """
        Delete a stack

        :param stack_id: Stack ID
        """
        self._client._delete('stacks/{0}'.format(stack_id))
Пример #4
0
class Resource(resource.Resource):

    """Scripts API methods"""

    @command(parser_options=dict(
        description='List all existing cluster scripts',
    ))
    @display_table(Script)
    def list(self):
        """
        List scripts that belong to the tenant specified in the client

        :returns: List of :class:`~lavaclient.api.response.Script` objects
        """
        return self._parse_response(
            self._client._get('scripts'),
            ScriptsResponse,
            wrapper='scripts')

    @command(
        parser_options=dict(
            description='Create a cluster script',
        ),
        name=argument(help='Descriptive name for this script'),
        url=argument(help='The URL from which the script may be downloaded'),
        script_type=argument(metavar='<type>',
                             choices=['post_init'],
                             help='The type of script, e.g post-init script')
    )
    @display_table(Script)
    def create(self, name, url, script_type):
        """
        Create a script. Currently only post-init scripts are supported.

        :param name: Script name
        :param url: The URL from which the script may be downloaded
        :param script_type: Script type; currently, must be 'post_init'
        :returns: :class:`~lavaclient.api.response.Script`
        """
        data = dict(
            name=name,
            url=url,
            type=script_type.upper(),
        )

        request_data = self._marshal_request(
            data, CreateScriptRequest, wrapper='script')

        return self._parse_response(
            self._client._post('scripts', json=request_data),
            ScriptResponse,
            wrapper='script')

    @command(
        parser_options=dict(
            description='Update an existing script',
        ),
        name=argument(help='Descriptive name for this script'),
        url=argument(help='The URL from which the script may be downloaded'),
        script_type=argument('--type',
                             choices=['post_init'],
                             help='The type of script, e.g post-init script')
    )
    @display_table(Script)
    def update(self, script_id, name=None, url=None, script_type=None):
        """
        Update an existing script.

        :param script_id: ID of existing script
        :param name: Script name
        :param url: The URL from which the script may be downloaded
        :param script_type: Script type; currently, must be 'post_init'
        :returns: :class:`~lavaclient.api.response.Script`
        """
        params = [('name', name),
                  ('url', url),
                  ('type', script_type.upper() if script_type else None)]
        data = {}
        for key, value in params:
            if value is not None:
                data[key] = value

        request_data = self._marshal_request(
            data, UpdateScriptRequest, wrapper='script')

        return self._parse_response(
            self._client._put('scripts/{0}'.format(script_id),
                              json=request_data),
            ScriptResponse,
            wrapper='script')

    @command(
        parser_options=dict(description='Delete a cluster script'),
        do_confirm=argument('--force', action='store_false',
                            help='Suppress delete confirmation dialog'),
    )
    def _delete(self, script_id, do_confirm=True):
        if do_confirm and not confirm('Delete script {0}?'.format(script_id)):
            return

        return self.delete(script_id)

    def delete(self, script_id):
        """
        Delete a script.

        :param script_id: ID of existing script
        """
        self._client._delete('scripts/{0}'.format(script_id))
Пример #5
0
class Resource(resource.Resource):
    """Credentials API methods"""
    def _list(self, type=None):
        url = 'credentials/' + type if type else 'credentials'
        resp = self._parse_response(self._client._get(url),
                                    CredentialsResponse,
                                    wrapper='credentials')

        return getattr(resp, type) if type else resp

    @command(parser_options=dict(description='List all existing credentials', )
             )
    @display_table(Credentials)
    def list(self):
        """
        List all credentials belonging to the tenant

        :returns: List of :class:`Credentials` objects
        """
        return self._list()

    @command(parser_options=dict(description='List all SSH keys'))
    @display_table(SSHKey)
    def list_ssh_keys(self):
        """
        List all SSH keys

        :returns: List of :class:`SSHKey` objects
        """
        return self._list(type='ssh_keys')

    @command(
        parser_options=dict(description='List all Cloud Files credentials'))
    @display_table(CloudFilesCredential)
    def list_cloud_files(self):
        """
        List all Cloud Files credentials

        :returns: List of :class:`CloudFilesCredential` objects
        """
        return self._list(type='cloud_files')

    @command(parser_options=dict(description='List all Amazon S3 credentials'))
    @display_table(S3Credential)
    def list_s3(self):
        """
        List all Amazon S3 credentials

        :returns: List of :class:`S3Credential` objects
        """
        return self._list(type='s3')

    @command(parser_options=dict(description='List all Ambari credentials'))
    @display_table(AmbariCredential)
    def list_ambari(self):
        """
        List all Ambari credentials

        :returns: List of :class:`AmbariCredential` objects
        """
        return self._list(type='ambari')

    def list_types(self):
        """
        List all credential types

        :returns: List of CredentialType objects
        """
        return self._parse_response(self._get('credentials/types'),
                                    CredentialTypesResponse,
                                    wrapper='credentials')

    @command(parser_options=dict(
        description='Upload a SSH public key for cluster logins'),
             name=argument(metavar='<name>',
                           help='Name to associate with the key'),
             public_key=argument(
                 help='SSH public key; must be either a file containing the '
                 'public key or the plaintext public key itself.'))
    @display_table(SSHKey)
    def create_ssh_key(self, name, public_key):
        """
        Upload an SSH public key for cluster logins

        :param name: The name to associate to the public key
        :param public_key: SSH public key in plaintext or path to key file
        :returns: :class:`SSHKey`
        """
        data = dict(
            key_name=name,
            public_key=file_or_string(public_key),
        )
        request_data = self._marshal_request(data,
                                             CreateSSHKeyRequest,
                                             wrapper='ssh_keys')

        resp = self._parse_response(self._client._post('credentials/ssh_keys',
                                                       json=request_data),
                                    CredentialResponse,
                                    wrapper='credentials')
        return resp.ssh_keys

    @command(parser_options=dict(
        description='Add credentials for a Cloud Files account'),
             username=argument(help='Cloud Files username'),
             api_key=argument(help='Cloud Files API key'))
    @display_table(CloudFilesCredential)
    def create_cloud_files(self, username, api_key):
        """
        Create credentials for Cloud Files access

        :param username: Cloud Files username
        :param api_key: Cloud Files API Key
        :returns: :class:`CloudFilesCredential`
        """
        data = dict(
            username=username,
            api_key=api_key,
        )
        request_data = self._marshal_request(data,
                                             CreateCloudFilesRequest,
                                             wrapper='cloud_files')

        resp = self._parse_response(self._client._post(
            'credentials/cloud_files', json=request_data),
                                    CredentialResponse,
                                    wrapper='credentials')
        return resp.cloud_files

    @command(parser_options=dict(description='Add credentials for Amazon S3'),
             access_key_id=argument(help='Amazon S3 access key id'),
             access_secret_key=argument(help='Amazon S3 access secret key'))
    @display_table(S3Credential)
    def create_s3(self, access_key_id, access_secret_key):
        """
        Create credentials for Amazon S3 access

        :param access_key_id: S3 access key id
        :param access_secret_key: S3 access secret key
        :returns: :class:`S3Credential`
        """
        data = dict(
            access_key_id=access_key_id,
            access_secret_key=access_secret_key,
        )
        request_data = self._marshal_request(data,
                                             CreateS3Request,
                                             wrapper='s3')

        resp = self._parse_response(self._client._post('credentials/s3',
                                                       json=request_data),
                                    CredentialResponse,
                                    wrapper='credentials')
        return resp.s3

    @command(parser_options=dict(description='Add credentials for Ambari'),
             username=argument(help='Ambari username'),
             ambari_password=argument(help='Password'))
    @display_table(AmbariCredential)
    def _create_ambari(self, username, ambari_password=None):
        if ambari_password is None:
            ambari_password = getpass('Password for {0}: '.format(username))

        return self.create_ambari(username, ambari_password)

    def create_ambari(self, username, password):
        """
        Create credentials for Ambari access

        :param username: Ambari username
        :param password: Password
        :returns: :class:`AmbariCredential`
        """
        data = dict(
            username=username,
            password=password,
        )
        request_data = self._marshal_request(data,
                                             CreateAmbariRequest,
                                             wrapper='ambari')

        resp = self._parse_response(self._client._post('credentials/ambari',
                                                       json=request_data),
                                    CredentialResponse,
                                    wrapper='credentials')
        return resp.ambari

    @command(parser_options=dict(description='Update SSH key'),
             name=argument(metavar='<name>', help='Name of existing SSH key'),
             public_key=argument(
                 help='SSH public key; must be either a file containing the '
                 'public key or the plaintext public key itself.'))
    @display_table(SSHKey)
    def update_ssh_key(self, name, public_key):
        """
        Upload an SSH public key for cluster logins

        :param name: The name of an existing SSH key
        :param public_key: SSH public key in plaintext or path to key file
        :returns: :class:`SSHKey`
        """
        data = dict(key_name=name, public_key=file_or_string(public_key))
        request_data = self._marshal_request(data,
                                             CreateSSHKeyRequest,
                                             wrapper='ssh_keys')

        resp = self._parse_response(self._client._put(
            'credentials/ssh_keys/{0}'.format(name), json=request_data),
                                    CredentialResponse,
                                    wrapper='credentials')
        return resp.ssh_keys

    @command(
        parser_options=dict(
            description='Add credentials for a Cloud Files account'),
        username=argument(help='Username for existing Cloud Files credential'),
        api_key=argument(help='Cloud Files API key'))
    @display_table(CloudFilesCredential)
    def update_cloud_files(self, username, api_key):
        """
        Update credentials for Cloud Files access

        :param username: Cloud Files username
        :param api_key: Cloud Files API Key
        :returns: :class:`CloudFilesCredential`
        """
        data = dict(username=username, api_key=api_key)
        request_data = self._marshal_request(data,
                                             CreateCloudFilesRequest,
                                             wrapper='cloud_files')

        resp = self._parse_response(self._client._put(
            'credentials/cloud_files/{0}'.format(username), json=request_data),
                                    CredentialResponse,
                                    wrapper='credentials')
        return resp.cloud_files

    @command(
        parser_options=dict(description='Update credentials for Amazon S3'),
        access_key_id=argument(
            help='Access Key Id for existing Amazon S3 credential'),
        access_secret_key=argument(help='Amazon S3 Access Secret Key'))
    @display_table(S3Credential)
    def update_s3(self, access_key_id, access_secret_key):
        """
        Update credentials for Amazon S3 access

        :param access_key_id: S3 access key id
        :param access_secret_key: S3 access secret Key
        :returns: :class:`S3Credential`
        """
        data = dict(access_key_id=access_key_id,
                    access_secret_key=access_secret_key)
        request_data = self._marshal_request(data,
                                             CreateS3Request,
                                             wrapper='s3')

        resp = self._parse_response(self._client._put(
            'credentials/s3/{0}'.format(access_key_id), json=request_data),
                                    CredentialResponse,
                                    wrapper='credentials')
        return resp.s3

    @command(parser_options=dict(description='Update credentials for Ambari'),
             username=argument(help='Username for existing Ambari credential'),
             ambari_password=argument(help='Password', required=False))
    @display_table(AmbariCredential)
    def _update_ambari(self, username, ambari_password=None):
        if ambari_password is None:
            ambari_password = getpass('Password for {0}: '.format(username))

        return self.update_ambari(username, ambari_password)

    def update_ambari(self, username, password):
        """
        Update credentials for Ambari access

        :param username: Ambari username
        :param password: Password
        :returns: :class:`AmbariCredential`
        """
        data = dict(username=username, password=password)
        request_data = self._marshal_request(data,
                                             CreateAmbariRequest,
                                             wrapper='ambari')

        resp = self._parse_response(self._client._put(
            'credentials/ambari/{0}'.format(username), json=request_data),
                                    CredentialResponse,
                                    wrapper='credentials')
        return resp.ambari

    @command(
        parser_options=dict(description='Delete an SSH key'),
        name=argument(help='SSH key name'),
        do_confirm=argument('--force',
                            action='store_false',
                            help='Suppress delete confirmation dialog'),
    )
    def _delete_ssh_key(self, name, do_confirm=True):
        if do_confirm and not confirm('Delete SSH key {0}?'.format(name)):
            return

        return self.delete_ssh_key(name)

    def delete_ssh_key(self, name):
        """
        Delete SSH key

        :param name: Name that identifies the SSH key
        """
        self._client._delete('credentials/ssh_keys/{0}'.format(name))

    @command(
        parser_options=dict(description='Delete a Cloud Files credential'),
        username=argument(help='Cloud Files username'),
        do_confirm=argument('--force',
                            action='store_false',
                            help='Suppress delete confirmation dialog'),
    )
    def _delete_cloud_files(self, username, do_confirm=True):
        if do_confirm and not confirm(
                'Delete Cloud Files username {0}?'.format(username)):
            return

        return self.delete_cloud_files(username)

    def delete_cloud_files(self, username):
        """
        Delete Cloud Files credential

        :param username: Cloud Files username
        """
        self._client._delete('credentials/cloud_files/{0}'.format(username))

    @command(
        parser_options=dict(description='Delete Amazon S3 credential'),
        access_key_id=argument(help='Amazon S3 access key id'),
        do_confirm=argument('--force',
                            action='store_false',
                            help='Suppress delete confirmation dialog'),
    )
    def _delete_s3(self, access_key_id, do_confirm=True):
        if do_confirm and not confirm(
                'Delete S3 access key {0}?'.format(access_key_id)):
            return

        return self.delete_s3(access_key_id)

    def delete_s3(self, access_key_id):
        """
        Delete Amazon s3 credential

        :param access_key_id: S3 access key id
        """
        self._client._delete('credentials/s3/{0}'.format(access_key_id))

    @command(
        parser_options=dict(description='Delete Ambari credential'),
        username=argument(help='Ambari username'),
        do_confirm=argument('--force',
                            action='store_false',
                            help='Suppress delete confirmation dialog'),
    )
    def _delete_ambari(self, username, do_confirm=True):
        if do_confirm and not confirm(
                'Delete Ambari user {0}?'.format(username)):
            return

        return self.delete_ambari(username)

    def delete_ambari(self, username):
        """
        Delete Ambari credential

        :param username: Ambari username
        """
        self._client._delete('credentials/ambari/{0}'.format(username))