import click
from osmclient import client
from osmclient.common.exceptions import ClientException, NotFound
+from osmclient.common.utils import validate_uuid4
from prettytable import PrettyTable
import yaml
import json
import pkg_resources
import logging
from datetime import datetime
+from typing import Any, Dict
def wrap_text(text, width):
return vim_name
+def create_config(config_file, json_string):
+ """
+ Combines a YAML or JSON file with a JSON string into a Python3 structure
+ It loads the YAML or JSON file 'cfile' into a first dictionary.
+ It loads the JSON string into a second dictionary.
+ Then it updates the first dictionary with the info in the second dictionary.
+ If the field is present in both cfile and cdict, the field in cdict prevails.
+ If both cfile and cdict are None, it returns an empty dict (i.e. {})
+ """
+ config = {}
+ if config_file:
+ with open(config_file, "r") as cf:
+ config = yaml.safe_load(cf.read())
+ if json_string:
+ cdict = yaml.safe_load(json_string)
+ for k, v in cdict.items():
+ config[k] = v
+ return config
+
+
@click.group(
context_settings=dict(help_option_names=["-h", "--help"], max_content_width=160)
)
envvar="OSM_HOSTNAME",
help="hostname of server. " + "Also can set OSM_HOSTNAME in environment",
)
-# @click.option('--sol005/--no-sol005',
-# default=True,
-# envvar='OSM_SOL005',
-# help='Use ETSI NFV SOL005 API (default) or the previous SO API. ' +
-# 'Also can set OSM_SOL005 in environment')
@click.option(
"--user",
default=None,
help="user domain name for keystone authentication (default to None). "
+ "Also can set OSM_USER_DOMAIN_NAME in environment",
)
-# @click.option('--so-port',
-# default=None,
-# envvar='OSM_SO_PORT',
-# help='hostname of server. ' +
-# 'Also can set OSM_SO_PORT in environment')
-# @click.option('--so-project',
-# default=None,
-# envvar='OSM_SO_PROJECT',
-# help='Project Name in SO. ' +
-# 'Also can set OSM_SO_PROJECT in environment')
-# @click.option('--ro-hostname',
-# default=None,
-# envvar='OSM_RO_HOSTNAME',
-# help='hostname of RO server. ' +
-# 'Also can set OSM_RO_HOSTNAME in environment')
-# @click.option('--ro-port',
-# default=None,
-# envvar='OSM_RO_PORT',
-# help='hostname of RO server. ' +
-# 'Also can set OSM_RO_PORT in environment')
@click.pass_context
def cli_osm(ctx, **kwargs):
global logger
exit(1)
# Remove None values
kwargs = {k: v for k, v in kwargs.items() if v is not None}
- # if so_port is not None:
- # kwargs['so_port']=so_port
- # if so_project is not None:
- # kwargs['so_project']=so_project
- # if ro_hostname is not None:
- # kwargs['ro_host']=ro_hostname
- # if ro_port is not None:
- # kwargs['ro_port']=ro_port
sol005 = os.getenv("OSM_SOL005", True)
- # if user is not None:
- # kwargs['user']=user
- # if password is not None:
- # kwargs['password']=password
- # if project is not None:
- # kwargs['project']=project
- # if all_projects:
- # kwargs['all_projects']=all_projects
- # if public is not None:
- # kwargs['public']=public
ctx.obj = client.Client(host=hostname, sol005=sol005, **kwargs)
logger = logging.getLogger("osmclient")
help="do not return the control immediately, but keep it "
"until the operation is completed, or timeout",
)
+@click.option("--timeout", default=None, help="ns deployment timeout")
@click.pass_context
def ns_create(
ctx,
config,
config_file,
wait,
+ timeout,
):
"""creates a new NS instance"""
logger.debug("")
ssh_keys=ssh_keys,
account=vim_account,
wait=wait,
+ timeout=timeout,
)
# except ClientException as e:
# print(str(e))
@click.option("--description", help="human readable description")
@click.option(
"--vim_account",
- help="list of VIM accounts (in the same VIM) that can reach this PDU",
+ help="list of VIM accounts (in the same VIM) that can reach this PDU\n"
+ + "The format for multiple VIMs is --vim_account <vim_account_id_1> "
+ + "--vim_account <vim_account_id_2> ... --vim_account <vim_account_id_N>",
multiple=True,
)
@click.option(
"--descriptor_file",
default=None,
- help="PDU descriptor file (as an alternative to using the other arguments",
+ help="PDU descriptor file (as an alternative to using the other arguments)",
)
@click.pass_context
def pdu_create(
):
"""creates a new Physical Deployment Unit (PDU)"""
logger.debug("")
- # try:
+
+ check_client_version(ctx.obj, ctx.command.name)
+
+ pdu = create_pdu_dictionary(
+ name, pdu_type, interface, description, vim_account, descriptor_file
+ )
+ ctx.obj.pdu.create(pdu)
+
+
+########################
+# UPDATE PDU operation #
+########################
+
+
+@cli_osm.command(
+ name="pdu-update", short_help="updates a Physical Deployment Unit to the catalog"
+)
+@click.argument("name")
+@click.option("--newname", help="New name for the Physical Deployment Unit")
+@click.option("--pdu_type", help="type of PDU (e.g. router, firewall, FW001)")
+@click.option(
+ "--interface",
+ help="interface(s) of the PDU: name=<NAME>,mgmt=<true|false>,ip-address=<IP_ADDRESS>"
+ + "[,type=<overlay|underlay>][,mac-address=<MAC_ADDRESS>][,vim-network-name=<VIM_NET_NAME>]",
+ multiple=True,
+)
+@click.option("--description", help="human readable description")
+@click.option(
+ "--vim_account",
+ help="list of VIM accounts (in the same VIM) that can reach this PDU\n"
+ + "The format for multiple VIMs is --vim_account <vim_account_id_1> "
+ + "--vim_account <vim_account_id_2> ... --vim_account <vim_account_id_N>",
+ multiple=True,
+)
+@click.option(
+ "--descriptor_file",
+ default=None,
+ help="PDU descriptor file (as an alternative to using the other arguments)",
+)
+@click.pass_context
+def pdu_update(
+ ctx, name, newname, pdu_type, interface, description, vim_account, descriptor_file
+):
+ """Updates a new Physical Deployment Unit (PDU)"""
+ logger.debug("")
+
check_client_version(ctx.obj, ctx.command.name)
+
+ update = True
+
+ if not newname:
+ newname = name
+
+ pdu = create_pdu_dictionary(
+ newname, pdu_type, interface, description, vim_account, descriptor_file, update
+ )
+ ctx.obj.pdu.update(name, pdu)
+
+
+def create_pdu_dictionary(
+ name, pdu_type, interface, description, vim_account, descriptor_file, update=False
+):
+
+ logger.debug("")
pdu = {}
+
if not descriptor_file:
- if not name:
- raise ClientException(
- 'in absence of descriptor file, option "--name" is mandatory'
- )
- if not pdu_type:
- raise ClientException(
- 'in absence of descriptor file, option "--pdu_type" is mandatory'
- )
- if not interface:
- raise ClientException(
- 'in absence of descriptor file, option "--interface" is mandatory (at least once)'
- )
- if not vim_account:
- raise ClientException(
- 'in absence of descriptor file, option "--vim_account" is mandatory (at least once)'
- )
+ if not update:
+ if not name:
+ raise ClientException(
+ 'in absence of descriptor file, option "--name" is mandatory'
+ )
+ if not pdu_type:
+ raise ClientException(
+ 'in absence of descriptor file, option "--pdu_type" is mandatory'
+ )
+ if not interface:
+ raise ClientException(
+ 'in absence of descriptor file, option "--interface" is mandatory (at least once)'
+ )
+ if not vim_account:
+ raise ClientException(
+ 'in absence of descriptor file, option "--vim_account" is mandatory (at least once)'
+ )
else:
with open(descriptor_file, "r") as df:
pdu = yaml.safe_load(df.read())
new_iface["mgmt"] = new_iface.get("mgmt", "false").lower() == "true"
ifaces_list.append(new_iface)
pdu["interfaces"] = ifaces_list
- ctx.obj.pdu.create(pdu)
- # except ClientException as e:
- # print(str(e))
- # exit(1)
+ return pdu
####################
@cli_osm.command(name="vim-create", short_help="creates a new VIM account")
-@click.option("--name", prompt=True, help="Name to create datacenter")
-@click.option("--user", prompt=True, help="VIM username")
+@click.option("--name", required=True, help="Name to create datacenter")
+@click.option("--user", default=None, help="VIM username")
+@click.option("--password", default=None, help="VIM password")
+@click.option("--auth_url", default=None, help="VIM url")
@click.option(
- "--password",
- prompt=True,
- hide_input=True,
- confirmation_prompt=True,
- help="VIM password",
+ "--tenant", "--project", "tenant", default=None, help="VIM tenant/project name"
)
-@click.option("--auth_url", prompt=True, help="VIM url")
-@click.option("--tenant", prompt=True, help="VIM tenant name")
@click.option("--config", default=None, help="VIM specific config parameters")
+@click.option(
+ "--config_file",
+ default=None,
+ help="VIM specific config parameters in YAML or JSON file",
+)
@click.option("--account_type", default="openstack", help="VIM type")
@click.option("--description", default=None, help="human readable description")
@click.option(
help="do not return the control immediately, but keep it "
"until the operation is completed, or timeout",
)
+@click.option("--vca", default=None, help="VCA to be used in this VIM account")
+@click.option(
+ "--creds", default=None, help="credentials file (only applycable for GCP VIM type)"
+)
+@click.option(
+ "--prometheus_config_file",
+ default=None,
+ help="Prometheus configuration to get VIM data",
+)
@click.pass_context
def vim_create(
ctx,
auth_url,
tenant,
config,
+ config_file,
account_type,
description,
sdn_controller,
sdn_port_mapping,
wait,
+ vca,
+ creds,
+ prometheus_config_file,
):
"""creates a new VIM account"""
logger.debug("")
if sdn_port_mapping:
check_client_version(ctx.obj, "--sdn_port_mapping")
vim = {}
+ if prometheus_config_file:
+ with open(prometheus_config_file) as prometheus_file:
+ prometheus_config_dict = json.load(prometheus_file)
+ vim["prometheus-config"] = prometheus_config_dict
+
vim["vim-username"] = user
vim["vim-password"] = password
vim["vim-url"] = auth_url
vim["vim-tenant-name"] = tenant
vim["vim-type"] = account_type
vim["description"] = description
- vim["config"] = config
- if sdn_controller or sdn_port_mapping:
- ctx.obj.vim.create(name, vim, sdn_controller, sdn_port_mapping, wait=wait)
- else:
- ctx.obj.vim.create(name, vim, wait=wait)
+ if vca:
+ vim["vca"] = vca
+ vim_config = create_config(config_file, config)
+ if creds:
+ with open(creds, "r") as cf:
+ vim_config["credentials"] = yaml.safe_load(cf.read())
+ ctx.obj.vim.create(
+ name, vim, vim_config, sdn_controller, sdn_port_mapping, wait=wait
+ )
# except ClientException as e:
# print(str(e))
# exit(1)
@click.option("--auth_url", help="VIM url")
@click.option("--tenant", help="VIM tenant name")
@click.option("--config", help="VIM specific config parameters")
+@click.option(
+ "--config_file",
+ default=None,
+ help="VIM specific config parameters in YAML or JSON file",
+)
@click.option("--account_type", help="VIM type")
@click.option("--description", help="human readable description")
@click.option(
help="do not return the control immediately, but keep it "
"until the operation is completed, or timeout",
)
+@click.option(
+ "--creds", default=None, help="credentials file (only applycable for GCP VIM type)"
+)
+@click.option(
+ "--prometheus_config_file",
+ default=None,
+ help="Prometheus configuration to get VIM data",
+)
@click.pass_context
def vim_update(
ctx,
auth_url,
tenant,
config,
+ config_file,
account_type,
description,
sdn_controller,
sdn_port_mapping,
wait,
+ creds,
+ prometheus_config_file,
):
"""updates a VIM account
vim["vim_type"] = account_type
if description:
vim["description"] = description
- if config:
- vim["config"] = config
- ctx.obj.vim.update(name, vim, sdn_controller, sdn_port_mapping, wait=wait)
+ vim_config = None
+ if config or config_file:
+ vim_config = create_config(config_file, config)
+ if creds:
+ with open(creds, "r") as cf:
+ vim_config["credentials"] = yaml.safe_load(cf.read())
+ if prometheus_config_file:
+ with open(prometheus_config_file) as prometheus_file:
+ prometheus_config_dict = json.load(prometheus_file)
+ vim["prometheus-config"] = prometheus_config_dict
+ logger.info(f"VIM: {vim}, VIM config: {vim_config}")
+ ctx.obj.vim.update(
+ name, vim, vim_config, sdn_controller, sdn_port_mapping, wait=wait
+ )
# except ClientException as e:
# print(str(e))
# exit(1)
if long:
if "vim_password" in vim:
vim["vim_password"] = "********"
+ if "config" in vim and "credentials" in vim["config"]:
+ vim["config"]["credentials"] = "********"
logger.debug("VIM details: {}".format(yaml.safe_dump(vim)))
vim_state = vim["_admin"].get("operationalState", "-")
error_details = "N/A"
multiple=True,
help="restricts the information to the fields in the filter",
)
+@click.option("--literal", is_flag=True, help="print literally, no pretty table")
@click.pass_context
-def vim_show(ctx, name, filter):
+def vim_show(ctx, name, filter, literal):
"""shows the details of a VIM account
NAME: name or ID of the VIM account
resp = ctx.obj.vim.get(name)
if "vim_password" in resp:
resp["vim_password"] = "********"
+ if "config" in resp and "credentials" in resp["config"]:
+ resp["config"]["credentials"] = "********"
# except ClientException as e:
# print(str(e))
# exit(1)
+ if literal:
+ print(yaml.safe_dump(resp, indent=4, default_flow_style=False))
+ return
table = PrettyTable(["key", "attribute"])
for k, v in list(resp.items()):
if not filter or k in filter:
check_client_version(ctx.obj, ctx.command.name)
resp = ctx.obj.wim.get(name)
if "password" in resp:
- resp["wim_password"] = "********"
+ resp["password"] = "********"
# except ClientException as e:
# print(str(e))
# exit(1)
help='''list of VIM networks, in JSON inline format, where the cluster is
accessible via L3 routing, e.g. "{(k8s_net1:vim_network1) [,(k8s_net2:vim_network2) ...]}"''',
)
+@click.option(
+ "--init-helm2/--skip-helm2",
+ required=False,
+ default=True,
+ help="Initialize helm v2",
+)
+@click.option(
+ "--init-helm3/--skip-helm3",
+ required=False,
+ default=True,
+ help="Initialize helm v3",
+)
+@click.option(
+ "--init-jujubundle/--skip-jujubundle",
+ required=False,
+ default=True,
+ help="Initialize juju-bundle",
+)
@click.option("--description", default=None, help="human readable description")
@click.option(
"--namespace",
default="kube-system",
help="namespace to be used for its operation, defaults to `kube-system`",
)
+@click.option(
+ "--wait",
+ required=False,
+ default=False,
+ is_flag=True,
+ help="do not return the control immediately, but keep it "
+ "until the operation is completed, or timeout",
+)
@click.option(
"--cni",
default=None,
# help='do not return the control immediately, but keep it until the operation is completed, or timeout')
@click.pass_context
def k8scluster_add(
- ctx, name, creds, version, vim, k8s_nets, description, namespace, cni
+ ctx,
+ name,
+ creds,
+ version,
+ vim,
+ k8s_nets,
+ init_helm2,
+ init_helm3,
+ init_jujubundle,
+ description,
+ namespace,
+ wait,
+ cni,
):
"""adds a K8s cluster to OSM
cluster["k8s_version"] = version
cluster["vim_account"] = vim
cluster["nets"] = yaml.safe_load(k8s_nets)
+ if not (init_helm2 and init_jujubundle and init_helm3):
+ cluster["deployment_methods"] = {
+ "helm-chart": init_helm2,
+ "juju-bundle": init_jujubundle,
+ "helm-chart-v3": init_helm3,
+ }
if description:
cluster["description"] = description
if namespace:
cluster["namespace"] = namespace
if cni:
cluster["cni"] = yaml.safe_load(cni)
- ctx.obj.k8scluster.create(name, cluster)
+ ctx.obj.k8scluster.create(name, cluster, wait)
# except ClientException as e:
# print(str(e))
# exit(1)
"--namespace",
help="namespace to be used for its operation, defaults to `kube-system`",
)
+@click.option(
+ "--wait",
+ required=False,
+ default=False,
+ is_flag=True,
+ help="do not return the control immediately, but keep it "
+ "until the operation is completed, or timeout",
+)
@click.option(
"--cni", help="list of CNI plugins, in JSON inline format, used in the cluster"
)
@click.pass_context
def k8scluster_update(
- ctx, name, newname, creds, version, vim, k8s_nets, description, namespace, cni
+ ctx, name, newname, creds, version, vim, k8s_nets, description, namespace, wait, cni
):
"""updates a K8s cluster
cluster["namespace"] = namespace
if cni:
cluster["cni"] = yaml.safe_load(cni)
- ctx.obj.k8scluster.update(name, cluster)
+ ctx.obj.k8scluster.update(name, cluster, wait)
# except ClientException as e:
# print(str(e))
# exit(1)
@click.option(
"--force", is_flag=True, help="forces the deletion from the DB (not recommended)"
)
-# @click.option('--wait',
-# is_flag=True,
-# help='do not return the control immediately, but keep it until the operation is completed, or timeout')
+@click.option(
+ "--wait",
+ required=False,
+ default=False,
+ is_flag=True,
+ help="do not return the control immediately, but keep it "
+ "until the operation is completed, or timeout",
+)
@click.pass_context
-def k8scluster_delete(ctx, name, force):
+def k8scluster_delete(ctx, name, force, wait):
"""deletes a K8s cluster
NAME: name or ID of the K8s cluster to be deleted
"""
# try:
check_client_version(ctx.obj, ctx.command.name)
- ctx.obj.k8scluster.delete(name, force=force)
+ ctx.obj.k8scluster.delete(name, force, wait)
# except ClientException as e:
# print(str(e))
# exit(1)
"Version",
"VIM",
"K8s-nets",
+ "Deployment methods",
"Operational State",
"Op. state (details)",
"Description",
cluster["k8s_version"],
vim_info,
json.dumps(cluster["nets"]),
+ json.dumps(cluster["deployment_methods"]),
cluster["_admin"]["operationalState"],
op_state_details,
trunc_text(cluster.get("description") or "", 40),
# exit(1)
+###########################
+# VCA operations
+###########################
+
+
+@cli_osm.command(name="vca-add", short_help="adds a VCA (Juju controller) to OSM")
+@click.argument("name")
+@click.option(
+ "--endpoints",
+ prompt=True,
+ help="Comma-separated list of IP or hostnames of the Juju controller",
+)
+@click.option("--user", prompt=True, help="Username with admin priviledges")
+@click.option("--secret", prompt=True, help="Password of the specified username")
+@click.option("--cacert", prompt=True, help="CA certificate")
+@click.option(
+ "--lxd-cloud",
+ prompt=True,
+ help="Name of the cloud that will be used for LXD containers (LXD proxy charms)",
+)
+@click.option(
+ "--lxd-credentials",
+ prompt=True,
+ help="Name of the cloud credentialsto be used for the LXD cloud",
+)
+@click.option(
+ "--k8s-cloud",
+ prompt=True,
+ help="Name of the cloud that will be used for K8s containers (K8s proxy charms)",
+)
+@click.option(
+ "--k8s-credentials",
+ prompt=True,
+ help="Name of the cloud credentialsto be used for the K8s cloud",
+)
+@click.option(
+ "--model-config",
+ default={},
+ help="Configuration options for the models",
+)
+@click.option("--description", default=None, help="human readable description")
+@click.pass_context
+def vca_add(
+ ctx,
+ name,
+ endpoints,
+ user,
+ secret,
+ cacert,
+ lxd_cloud,
+ lxd_credentials,
+ k8s_cloud,
+ k8s_credentials,
+ model_config,
+ description,
+):
+ """adds a VCA to OSM
+
+ NAME: name of the VCA
+ """
+ check_client_version(ctx.obj, ctx.command.name)
+ vca = {}
+ vca["name"] = name
+ vca["endpoints"] = endpoints.split(",")
+ vca["user"] = user
+ vca["secret"] = secret
+ vca["cacert"] = cacert
+ vca["lxd-cloud"] = lxd_cloud
+ vca["lxd-credentials"] = lxd_credentials
+ vca["k8s-cloud"] = k8s_cloud
+ vca["k8s-credentials"] = k8s_credentials
+ if description:
+ vca["description"] = description
+ if model_config:
+ model_config = load(model_config)
+ vca["model-config"] = model_config
+ ctx.obj.vca.create(name, vca)
+
+
+def load(data: Any):
+ if os.path.isfile(data):
+ return load_file(data)
+ else:
+ try:
+ return json.loads(data)
+ except ValueError as e:
+ raise ClientException(e)
+
+
+def load_file(file_path: str) -> Dict:
+ content = None
+ with open(file_path, "r") as f:
+ content = f.read()
+ try:
+ return yaml.safe_load(content)
+ except yaml.scanner.ScannerError:
+ pass
+ try:
+ return json.loads(content)
+ except ValueError:
+ pass
+ raise ClientException(f"{file_path} must be a valid yaml or json file")
+
+
+@cli_osm.command(name="vca-update", short_help="updates a K8s cluster")
+@click.argument("name")
+@click.option(
+ "--endpoints", help="Comma-separated list of IP or hostnames of the Juju controller"
+)
+@click.option("--user", help="Username with admin priviledges")
+@click.option("--secret", help="Password of the specified username")
+@click.option("--cacert", help="CA certificate")
+@click.option(
+ "--lxd-cloud",
+ help="Name of the cloud that will be used for LXD containers (LXD proxy charms)",
+)
+@click.option(
+ "--lxd-credentials",
+ help="Name of the cloud credentialsto be used for the LXD cloud",
+)
+@click.option(
+ "--k8s-cloud",
+ help="Name of the cloud that will be used for K8s containers (K8s proxy charms)",
+)
+@click.option(
+ "--k8s-credentials",
+ help="Name of the cloud credentialsto be used for the K8s cloud",
+)
+@click.option(
+ "--model-config",
+ help="Configuration options for the models",
+)
+@click.option("--description", default=None, help="human readable description")
+@click.pass_context
+def vca_update(
+ ctx,
+ name,
+ endpoints,
+ user,
+ secret,
+ cacert,
+ lxd_cloud,
+ lxd_credentials,
+ k8s_cloud,
+ k8s_credentials,
+ model_config,
+ description,
+):
+ """updates a K8s cluster
+
+ NAME: name or ID of the K8s cluster
+ """
+ check_client_version(ctx.obj, ctx.command.name)
+ vca = {}
+ vca["name"] = name
+ if endpoints:
+ vca["endpoints"] = endpoints.split(",")
+ if user:
+ vca["user"] = user
+ if secret:
+ vca["secret"] = secret
+ if cacert:
+ vca["cacert"] = cacert
+ if lxd_cloud:
+ vca["lxd-cloud"] = lxd_cloud
+ if lxd_credentials:
+ vca["lxd-credentials"] = lxd_credentials
+ if k8s_cloud:
+ vca["k8s-cloud"] = k8s_cloud
+ if k8s_credentials:
+ vca["k8s-credentials"] = k8s_credentials
+ if description:
+ vca["description"] = description
+ if model_config:
+ model_config = load(model_config)
+ vca["model-config"] = model_config
+ ctx.obj.vca.update(name, vca)
+
+
+@cli_osm.command(name="vca-delete", short_help="deletes a K8s cluster")
+@click.argument("name")
+@click.option(
+ "--force", is_flag=True, help="forces the deletion from the DB (not recommended)"
+)
+@click.pass_context
+def vca_delete(ctx, name, force):
+ """deletes a K8s cluster
+
+ NAME: name or ID of the K8s cluster to be deleted
+ """
+ check_client_version(ctx.obj, ctx.command.name)
+ ctx.obj.vca.delete(name, force=force)
+
+
+@cli_osm.command(name="vca-list")
+@click.option(
+ "--filter",
+ default=None,
+ multiple=True,
+ help="restricts the list to the VCAs matching the filter",
+)
+@click.option("--literal", is_flag=True, help="print literally, no pretty table")
+@click.option("--long", is_flag=True, help="get more details")
+@click.pass_context
+def vca_list(ctx, filter, literal, long):
+ """list VCAs"""
+ check_client_version(ctx.obj, ctx.command.name)
+ if filter:
+ filter = "&".join(filter)
+ resp = ctx.obj.vca.list(filter)
+ if literal:
+ print(yaml.safe_dump(resp, indent=4, default_flow_style=False))
+ return
+ if long:
+ table = PrettyTable(
+ ["Name", "Id", "Project", "Operational State", "Detailed Status"]
+ )
+ project_list = ctx.obj.project.list()
+ else:
+ table = PrettyTable(["Name", "Id", "Operational State"])
+ for vca in resp:
+ logger.debug("VCA details: {}".format(yaml.safe_dump(vca)))
+ if long:
+ project_id, project_name = get_project(project_list, vca)
+ detailed_status = vca.get("_admin", {}).get("detailed-status", "-")
+ table.add_row(
+ [
+ vca["name"],
+ vca["_id"],
+ project_name,
+ vca.get("_admin", {}).get("operationalState", "-"),
+ wrap_text(text=detailed_status, width=40),
+ ]
+ )
+ else:
+ table.add_row(
+ [
+ vca["name"],
+ vca["_id"],
+ vca.get("_admin", {}).get("operationalState", "-"),
+ ]
+ )
+ table.align = "l"
+ print(table)
+
+
+@cli_osm.command(name="vca-show", short_help="shows the details of a K8s cluster")
+@click.argument("name")
+@click.option("--literal", is_flag=True, help="print literally, no pretty table")
+@click.pass_context
+def vca_show(ctx, name, literal):
+ """shows the details of a K8s cluster
+
+ NAME: name or ID of the K8s cluster
+ """
+ # try:
+ resp = ctx.obj.vca.get(name)
+ if literal:
+ print(yaml.safe_dump(resp, indent=4, default_flow_style=False))
+ return
+ table = PrettyTable(["key", "attribute"])
+ for k, v in list(resp.items()):
+ table.add_row([k, wrap_text(text=json.dumps(v, indent=2), width=100)])
+ table.align = "l"
+ print(table)
+
+
###########################
# Repo operations
###########################
multiple=True,
help="remove role(s) in a project. Can be used several times: 'project,role1[,role2,...]'",
)
+@click.option("--change_password", "change_password", help="user's current password")
+@click.option(
+ "--new_password",
+ "new_password",
+ help="user's new password to update in expiry condition",
+)
@click.pass_context
def user_update(
ctx,
remove_project,
add_project_role,
remove_project_role,
+ change_password,
+ new_password,
):
"""Update a user information
REMOVE_PROJECT: deleting mappings for project/role(s)
ADD_PROJECT_ROLE: adding mappings for project/role(s)
REMOVE_PROJECT_ROLE: removing mappings for project/role(s)
+ CHANGE_PASSWORD: user's current password to change
+ NEW_PASSWORD: user's new password to update in expiry condition
"""
logger.debug("")
user = {}
user["remove-project"] = remove_project
user["add-project-role"] = add_project_role
user["remove-project-role"] = remove_project_role
+ user["change_password"] = change_password
+ user["new_password"] = new_password
# try:
check_client_version(ctx.obj, ctx.command.name)
- ctx.obj.user.update(username, user)
+ if not user.get("change_password"):
+ ctx.obj.user.update(username, user)
+ else:
+ ctx.obj.user.update(username, user, pwd_change=True)
# except ClientException as e:
# print(str(e))
# exit(1)
# exit(1)
+#################
+# Subscription operations
+#################
+
+
+@cli_osm.command(
+ name="subscription-create",
+ short_help="creates a new subscription to a specific event",
+)
+@click.option(
+ "--event_type",
+ # type=click.Choice(['ns', 'nspkg', 'vnfpkg'], case_sensitive=False))
+ type=click.Choice(["ns"], case_sensitive=False),
+ help="event type to be subscribed (for the moment, only ns is supported)",
+)
+@click.option("--event", default=None, help="specific yaml configuration for the event")
+@click.option(
+ "--event_file", default=None, help="specific yaml configuration file for the event"
+)
+@click.pass_context
+def subscription_create(ctx, event_type, event, event_file):
+ """creates a new subscription to a specific event"""
+ logger.debug("")
+ check_client_version(ctx.obj, ctx.command.name)
+ if event_file:
+ if event:
+ raise ClientException(
+ '"--event" option is incompatible with "--event_file" option'
+ )
+ with open(event_file, "r") as cf:
+ event = cf.read()
+ ctx.obj.subscription.create(event_type, event)
+
+
+@cli_osm.command(name="subscription-delete", short_help="deletes a subscription")
+@click.option(
+ "--event_type",
+ # type=click.Choice(['ns', 'nspkg', 'vnfpkg'], case_sensitive=False))
+ type=click.Choice(["ns"], case_sensitive=False),
+ help="event type to be subscribed (for the moment, only ns is supported)",
+)
+@click.argument("subscription_id")
+@click.option(
+ "--force", is_flag=True, help="forces the deletion bypassing pre-conditions"
+)
+@click.pass_context
+def subscription_delete(ctx, event_type, subscription_id, force):
+ """deletes a subscription
+
+ SUBSCRIPTION_ID: ID of the subscription to be deleted
+ """
+ logger.debug("")
+ check_client_version(ctx.obj, ctx.command.name)
+ ctx.obj.subscription.delete(event_type, subscription_id, force)
+
+
+@cli_osm.command(name="subscription-list", short_help="list all subscriptions")
+@click.option(
+ "--event_type",
+ # type=click.Choice(['ns', 'nspkg', 'vnfpkg'], case_sensitive=False))
+ type=click.Choice(["ns"], case_sensitive=False),
+ help="event type to be subscribed (for the moment, only ns is supported)",
+)
+@click.option(
+ "--filter",
+ default=None,
+ multiple=True,
+ help="restricts the list to the subscriptions matching the filter",
+)
+@click.pass_context
+def subscription_list(ctx, event_type, filter):
+ """list all subscriptions"""
+ logger.debug("")
+ check_client_version(ctx.obj, ctx.command.name)
+ if filter:
+ filter = "&".join(filter)
+ resp = ctx.obj.subscription.list(event_type, filter)
+ table = PrettyTable(["id", "filter", "CallbackUri"])
+ for sub in resp:
+ table.add_row(
+ [
+ sub["_id"],
+ wrap_text(text=json.dumps(sub["filter"], indent=2), width=70),
+ sub["CallbackUri"],
+ ]
+ )
+ table.align = "l"
+ print(table)
+
+
+@cli_osm.command(
+ name="subscription-show", short_help="shows the details of a subscription"
+)
+@click.argument("subscription_id")
+@click.option(
+ "--event_type",
+ # type=click.Choice(['ns', 'nspkg', 'vnfpkg'], case_sensitive=False))
+ type=click.Choice(["ns"], case_sensitive=False),
+ help="event type to be subscribed (for the moment, only ns is supported)",
+)
+@click.option(
+ "--filter",
+ multiple=True,
+ help="restricts the information to the fields in the filter",
+)
+@click.pass_context
+def subscription_show(ctx, event_type, subscription_id, filter):
+ """shows the details of a subscription
+
+ SUBSCRIPTION_ID: ID of the subscription
+ """
+ logger.debug("")
+ # try:
+ resp = ctx.obj.subscription.get(subscription_id)
+ table = PrettyTable(["key", "attribute"])
+ for k, v in list(resp.items()):
+ if not filter or k in filter:
+ table.add_row([k, wrap_text(text=json.dumps(v, indent=2), width=100)])
+ table.align = "l"
+ print(table)
+
+
####################
# Other operations
####################
# exit(1)
+@cli_osm.command(
+ name="ns-update", short_help="executes an update of a Network Service."
+)
+@click.argument("ns_name")
+@click.option(
+ "--updatetype", required=True, type=str, help="available types: CHANGE_VNFPKG"
+)
+@click.option(
+ "--config",
+ required=True,
+ type=str,
+ help="extra information for update operation as YAML/JSON inline string as --config"
+ " '{changeVnfPackageData:[{vnfInstanceId: xxx, vnfdId: yyy}]}'",
+)
+@click.option(
+ "--timeout", required=False, default=None, type=int, help="timeout in seconds"
+)
+@click.option(
+ "--wait",
+ required=False,
+ default=False,
+ is_flag=True,
+ help="do not return the control immediately, but keep it until the operation is completed, or timeout",
+)
+@click.pass_context
+def update(ctx, ns_name, updatetype, config, timeout, wait):
+ """Executes an update of a Network Service.
+
+ The update will check new revisions of the Network Functions that are part of the
+ Network Service, and it will update them if needed.
+ Sample update command: osm ns-update ns_instance_id --updatetype CHANGE_VNFPKG
+ --config '{changeVnfPackageData: [{vnfInstanceId: id_x,vnfdId: id_y}]}' --timeout 300 --wait
+
+ NS_NAME: Network service instance name or ID.
+
+ """
+ op_data = {
+ "timeout": timeout,
+ "updateType": updatetype,
+ }
+ if config:
+ op_data["config"] = yaml.safe_load(config)
+
+ check_client_version(ctx.obj, ctx.command.name)
+ ctx.obj.ns.update(ns_name, op_data, wait=wait)
+
+
+def iterator_split(iterator, separators):
+ """
+ Splits a tuple or list into several lists whenever a separator is found
+ For instance, the following tuple will be separated with the separator "--vnf" as follows.
+ From:
+ ("--vnf", "A", "--cause", "cause_A", "--vdu", "vdu_A1", "--vnf", "B", "--cause", "cause_B", ...
+ "--vdu", "vdu_B1", "--count_index", "1", "--run-day1", "--vdu", "vdu_B1", "--count_index", "2")
+ To:
+ [
+ ("--vnf", "A", "--cause", "cause_A", "--vdu", "vdu_A1"),
+ ("--vnf", "B", "--cause", "cause_B", "--vdu", "vdu_B1", "--count_index", "1", "--run-day1", ...
+ "--vdu", "vdu_B1", "--count_index", "2")
+ ]
+
+ Returns as many lists as separators are found
+ """
+ logger.debug("")
+ if iterator[0] not in separators:
+ raise ClientException(f"Expected one of {separators}. Received: {iterator[0]}.")
+ list_of_lists = []
+ first = 0
+ for i in range(len(iterator)):
+ if iterator[i] in separators:
+ if i == first:
+ continue
+ if i - first < 2:
+ raise ClientException(
+ f"Expected at least one argument after separator (possible separators: {separators})."
+ )
+ list_of_lists.append(list(iterator[first:i]))
+ first = i
+ if (len(iterator) - first) < 2:
+ raise ClientException(
+ f"Expected at least one argument after separator (possible separators: {separators})."
+ )
+ else:
+ list_of_lists.append(list(iterator[first : len(iterator)]))
+ # logger.debug(f"List of lists: {list_of_lists}")
+ return list_of_lists
+
+
+def process_common_heal_params(heal_vnf_dict, args):
+ logger.debug("")
+ current_item = "vnf"
+ i = 0
+ while i < len(args):
+ if args[i] == "--cause":
+ if (i + 1 >= len(args)) or args[i + 1].startswith("--"):
+ raise ClientException("No cause was provided after --cause")
+ heal_vnf_dict["cause"] = args[i + 1]
+ i = i + 2
+ continue
+ if args[i] == "--run-day1":
+ if current_item == "vnf":
+ if "additionalParams" not in heal_vnf_dict:
+ heal_vnf_dict["additionalParams"] = {}
+ heal_vnf_dict["additionalParams"]["run-day1"] = True
+ else:
+ # if current_item == "vdu"
+ heal_vnf_dict["additionalParams"]["vdu"][-1]["run-day1"] = True
+ i = i + 1
+ continue
+ if args[i] == "--vdu":
+ if "additionalParams" not in heal_vnf_dict:
+ heal_vnf_dict["additionalParams"] = {}
+ heal_vnf_dict["additionalParams"]["vdu"] = []
+ if (i + 1 >= len(args)) or args[i + 1].startswith("--"):
+ raise ClientException("No VDU ID was provided after --vdu")
+ heal_vnf_dict["additionalParams"]["vdu"].append({"vdu-id": args[i + 1]})
+ current_item = "vdu"
+ i = i + 2
+ continue
+ if args[i] == "--count-index":
+ if current_item == "vnf":
+ raise ClientException(
+ "Option --count-index only applies to VDU, not to VNF"
+ )
+ if (i + 1 >= len(args)) or args[i + 1].startswith("--"):
+ raise ClientException("No count index was provided after --count-index")
+ heal_vnf_dict["additionalParams"]["vdu"][-1]["count-index"] = int(
+ args[i + 1]
+ )
+ i = i + 2
+ continue
+ i = i + 1
+ return
+
+
+def process_ns_heal_params(ctx, param, value):
+ """
+ Processes the params in the command ns-heal
+ Click does not allow advanced patterns for positional options like this:
+ --vnf volumes_vnf --cause "Heal several_volumes-VM of several_volumes_vnf"
+ --vdu several_volumes-VM
+ --vnf charm_vnf --cause "Heal two VMs of native_manual_scale_charm_vnf"
+ --vdu mgmtVM --count-index 1 --run-day1
+ --vdu mgmtVM --count-index 2
+
+ It returns the dictionary with all the params stored in ctx.params["heal_params"]
+ """
+ logger.debug("")
+ # logger.debug(f"Args: {value}")
+ if param.name != "args":
+ raise ClientException(f"Unexpected param: {param.name}")
+ # Split the tuple "value" by "--vnf"
+ vnfs = iterator_split(value, ["--vnf"])
+ logger.debug(f"VNFs: {vnfs}")
+ heal_dict = {}
+ heal_dict["healVnfData"] = []
+ for vnf in vnfs:
+ # logger.debug(f"VNF: {vnf}")
+ heal_vnf = {}
+ if vnf[1].startswith("--"):
+ raise ClientException("Expected a VNF_ID after --vnf")
+ heal_vnf["vnfInstanceId"] = vnf[1]
+ process_common_heal_params(heal_vnf, vnf[2:])
+ heal_dict["healVnfData"].append(heal_vnf)
+ ctx.params["heal_params"] = heal_dict
+ return
+
+
+@cli_osm.command(
+ name="ns-heal",
+ short_help="heals (recreates) VNFs or VDUs of a NS instance",
+ context_settings=dict(
+ ignore_unknown_options=True,
+ ),
+)
+@click.argument("ns_name")
+@click.argument(
+ "args",
+ nargs=-1,
+ type=click.UNPROCESSED,
+ callback=process_ns_heal_params,
+)
+@click.option("--timeout", type=int, default=None, help="timeout in seconds")
+@click.option(
+ "--wait",
+ default=False,
+ is_flag=True,
+ help="do not return the control immediately, but keep it until the operation is completed, or timeout",
+)
+@click.pass_context
+def ns_heal(ctx, ns_name, args, heal_params, timeout, wait):
+ """heals (recreates) VNFs or VDUs of a NS instance
+
+ NS_NAME: name or ID of the NS instance
+
+ \b
+ Options:
+ --vnf TEXT VNF instance ID or VNF id in the NS [required]
+ --cause TEXT human readable cause of the healing
+ --run-day1 indicates whether or not to run day1 primitives for the VNF/VDU
+ --vdu TEXT vdu-id
+ --count-index INTEGER count-index
+
+ \b
+ Example:
+ osm ns-heal NS_NAME|NS_ID --vnf volumes_vnf --cause "Heal several_volumes-VM of several_volumes_vnf"
+ --vdu several_volumes-VM
+ --vnf charm_vnf --cause "Heal two VMs of native_manual_scale_charm_vnf"
+ --vdu mgmtVM --count-index 1 --run-day1
+ --vdu mgmtVM --count-index 2
+ """
+ logger.debug("")
+ heal_dict = ctx.params["heal_params"]
+ logger.debug(f"Heal dict:\n{yaml.safe_dump(heal_dict)}")
+ # replace VNF id in the NS by the VNF instance ID
+ for vnf in heal_dict["healVnfData"]:
+ vnf_id = vnf["vnfInstanceId"]
+ if not validate_uuid4(vnf_id):
+ vnf_filter = f"member-vnf-index-ref={vnf_id}"
+ vnf_list = ctx.obj.vnf.list(ns=ns_name, filter=vnf_filter)
+ if len(vnf_list) == 0:
+ raise ClientException(
+ f"No VNF found in NS {ns_name} with filter {vnf_filter}"
+ )
+ elif len(vnf_list) == 1:
+ vnf["vnfInstanceId"] = vnf_list[0]["_id"]
+ else:
+ raise ClientException(
+ f"More than 1 VNF found in NS {ns_name} with filter {vnf_filter}"
+ )
+ logger.debug(f"Heal dict:\n{yaml.safe_dump(heal_dict)}")
+ check_client_version(ctx.obj, ctx.command.name)
+ ctx.obj.ns.heal(ns_name, heal_dict, wait, timeout)
+ exit(0)
+
+
+def process_vnf_heal_params(ctx, param, value):
+ """
+ Processes the params in the command vnf-heal
+ Click does not allow advanced patterns for positional options like this:
+ --vdu mgmtVM --count-index 1 --run-day1 --vdu mgmtVM --count-index 2
+
+ It returns the dictionary with all the params stored in ctx.params["heal_params"]
+ """
+ logger.debug("")
+ # logger.debug(f"Args: {value}")
+ if param.name != "args":
+ raise ClientException(f"Unexpected param: {param.name}")
+ # Split the tuple "value" by "--vnf"
+ vnf = value
+ heal_dict = {}
+ heal_dict["healVnfData"] = []
+ logger.debug(f"VNF: {vnf}")
+ heal_vnf = {"vnfInstanceId": "id_to_be_substituted"}
+ process_common_heal_params(heal_vnf, vnf)
+ heal_dict["healVnfData"].append(heal_vnf)
+ ctx.params["heal_params"] = heal_dict
+ return
+
+
+@cli_osm.command(
+ name="vnf-heal",
+ short_help="heals (recreates) a VNF instance or the VDUs of a VNF instance",
+ context_settings=dict(
+ ignore_unknown_options=True,
+ ),
+)
+@click.argument("vnf_name")
+@click.argument(
+ "args",
+ nargs=-1,
+ type=click.UNPROCESSED,
+ callback=process_vnf_heal_params,
+)
+@click.option("--timeout", type=int, default=None, help="timeout in seconds")
+@click.option(
+ "--wait",
+ default=False,
+ is_flag=True,
+ help="do not return the control immediately, but keep it until the operation is completed, or timeout",
+)
+@click.pass_context
+def vnf_heal2(
+ ctx,
+ vnf_name,
+ args,
+ heal_params,
+ timeout,
+ wait,
+):
+ """heals (recreates) a VNF instance or the VDUs of a VNF instance
+
+ VNF_NAME: name or ID of the VNF instance
+
+ \b
+ Options:
+ --cause TEXT human readable cause of the healing of the VNF
+ --run-day1 indicates whether or not to run day1 primitives for the VNF/VDU
+ --vdu TEXT vdu-id
+ --count-index INTEGER count-index
+
+ \b
+ Example:
+ osm vnf-heal VNF_INSTANCE_ID --vdu mgmtVM --count-index 1 --run-day1
+ --vdu mgmtVM --count-index 2
+ """
+ logger.debug("")
+ heal_dict = ctx.params["heal_params"]
+ heal_dict["healVnfData"][-1]["vnfInstanceId"] = vnf_name
+ logger.debug(f"Heal dict:\n{yaml.safe_dump(heal_dict)}")
+ check_client_version(ctx.obj, ctx.command.name)
+ vnfr = ctx.obj.vnf.get(vnf_name)
+ ns_id = vnfr["nsr-id-ref"]
+ ctx.obj.ns.heal(ns_id, heal_dict, wait, timeout)
+ exit(0)
+
+
+@cli_osm.command(name="alarm-show", short_help="show alarm details")
+@click.argument("uuid")
+@click.pass_context
+def alarm_show(ctx, uuid):
+ """Show alarm's detail information"""
+
+ check_client_version(ctx.obj, ctx.command.name)
+ resp = ctx.obj.ns.get_alarm(uuid=uuid)
+ alarm_filter = [
+ "uuid",
+ "name",
+ "metric",
+ "statistic",
+ "threshold",
+ "operation",
+ "ns-id",
+ "vnf-id",
+ "vdu_name",
+ "action",
+ "status",
+ ]
+ table = PrettyTable(["key", "attribute"])
+ try:
+ # Arrange and return the response data
+ alarm = resp.replace("ObjectId", "")
+ for key in alarm_filter:
+ if key == "uuid":
+ value = alarm.get(key)
+ key = "alarm-id"
+ elif key == "name":
+ value = alarm.get(key)
+ key = "alarm-name"
+ elif key == "ns-id":
+ value = alarm["tags"].get("ns_id")
+ elif key == "vdu_name":
+ value = alarm["tags"].get("vdu_name")
+ elif key == "status":
+ value = alarm["alarm_status"]
+ else:
+ value = alarm[key]
+ table.add_row([key, wrap_text(text=json.dumps(value, indent=2), width=100)])
+ table.align = "l"
+ print(table)
+ except Exception:
+ print(resp)
+
+
+# List alarm
+@cli_osm.command(name="alarm-list", short_help="list all alarms")
+@click.option(
+ "--ns_id", default=None, required=False, help="List out alarm for given ns id"
+)
+@click.pass_context
+def alarm_list(ctx, ns_id):
+ """list all alarm"""
+
+ check_client_version(ctx.obj, ctx.command.name)
+ project_name = os.getenv("OSM_PROJECT", "admin")
+ resp = ctx.obj.ns.get_alarm(project_name=project_name, ns_id=ns_id)
+
+ table = PrettyTable(
+ ["alarm-id", "metric", "threshold", "operation", "action", "status"]
+ )
+ if resp:
+ # return the response data in a table
+ resp = resp.replace("ObjectId", "")
+ for alarm in resp:
+ table.add_row(
+ [
+ wrap_text(text=str(alarm["uuid"]), width=38),
+ alarm["metric"],
+ alarm["threshold"],
+ alarm["operation"],
+ wrap_text(text=alarm["action"], width=25),
+ alarm["alarm_status"],
+ ]
+ )
+ table.align = "l"
+ print(table)
+
+
+# Update alarm
+@cli_osm.command(name="alarm-update", short_help="Update a alarm")
+@click.argument("uuid")
+@click.option("--threshold", default=None, help="Alarm threshold")
+@click.option("--is_enable", default=None, type=bool, help="enable or disable alarm")
+@click.pass_context
+def alarm_update(ctx, uuid, threshold, is_enable):
+ """
+ Update alarm
+
+ """
+ if not threshold and is_enable is None:
+ raise ClientException(
+ "Please provide option to update i.e threshold or is_enable"
+ )
+ ctx.obj.ns.update_alarm(uuid, threshold, is_enable)
+
+
##############################
# Role Management Operations #
##############################
@click.option(
"--netslice-vlds", default=1, help="(NST) Number of netslice vlds. Default 1"
)
+@click.option(
+ "--old",
+ default=False,
+ is_flag=True,
+ help="Flag to create a descriptor using the previous OSM format (pre SOL006, OSM<9)",
+)
@click.pass_context
def package_create(
ctx,
detailed,
netslice_subnets,
netslice_vlds,
+ old,
):
"""
Creates an OSM NS, VNF, NST package
detailed=detailed,
netslice_subnets=netslice_subnets,
netslice_vlds=netslice_vlds,
+ old=old,
)
print(resp)
# except ClientException as inst:
def cli():
try:
- cli_osm()
+ cli_osm() # pylint: disable=no-value-for-parameter
exit(0)
except pycurl.error as exc:
print(exc)