#!/usr/bin/env python
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.

"""Javelin is a tool for creating, verifying, and deleting a small set of
resources in a declarative way.

Javelin is meant to be used as a way to validate quickly that resources can
survive an upgrade process.

Authentication
--------------

Javelin will be creating (and removing) users and tenants so it needs the admin
credentials of your cloud to operate properly. The corresponding info can be
given the usual way, either through CLI options or environment variables.

You're probably familiar with these, but just in case::

    +----------+------------------+----------------------+
    | Param    | CLI              | Environment Variable |
    +----------+------------------+----------------------+
    | Username | --os-username    | OS_USERNAME          |
    | Password | --os-password    | OS_PASSWORD          |
    | Tenant   | --os-tenant-name | OS_TENANT_NAME       |
    +----------+------------------+----------------------+


Runtime Arguments
-----------------

**-m/--mode**: (Required) Has to be one of 'check', 'create' or 'destroy'. It
indicates which actions javelin is going to perform.

**-r/--resources**: (Required) The path to a YAML file describing the resources
used by Javelin.

**-d/--devstack-base**: (Required) The path to the devstack repo used to
retrieve artefacts (like images) that will be referenced in the resource files.

**-c/--config-file**: (Optional) The path to a valid Tempest config file
describing your cloud. Javelin may use this to determine if certain services
are enabled and modify its behavior accordingly.


Resource file
-------------

The resource file is a valid YAML file describing the resources that will be
created, checked and destroyed by javelin. Here's a canonical example of a
resource file::

  tenants:
    - javelin
    - discuss

  users:
    - name: javelin
      pass: gungnir
      tenant: javelin
    - name: javelin2
      pass: gungnir2
      tenant: discuss

  # resources that we want to create
  images:
    - name: javelin_cirros
      owner: javelin
      file: cirros-0.3.2-x86_64-blank.img
      disk_format: ami
      container_format: ami
      aki: cirros-0.3.2-x86_64-vmlinuz
      ari: cirros-0.3.2-x86_64-initrd

  servers:
    - name: peltast
      owner: javelin
      flavor: m1.small
      image: javelin_cirros
      floating_ip_pool: public
    - name: hoplite
      owner: javelin
      flavor: m1.medium
      image: javelin_cirros


An important piece of the resource definition is the *owner* field, which is
the user (that we've created) that is the owner of that resource. All
operations on that resource will happen as that regular user to ensure that
admin level access does not mask issues.

The check phase will act like a unit test, using well known assert methods to
verify that the correct resources exist.

"""

import argparse
import collections
import datetime
import os
import sys
import unittest

import netaddr
from oslo_log import log as logging
from oslo_utils import timeutils
import six
import yaml

from tempest.common import identity
from tempest.common import waiters
from tempest import config
from tempest.lib import auth
from tempest.lib import exceptions as lib_exc
from tempest.lib.services.compute import flavors_client
from tempest.lib.services.compute import floating_ips_client
from tempest.lib.services.compute import security_group_rules_client
from tempest.lib.services.compute import security_groups_client
from tempest.lib.services.compute import servers_client
from tempest.lib.services.network import subnets_client
from tempest.services.identity.v2.json import identity_client
from tempest.services.identity.v2.json import roles_client
from tempest.services.identity.v2.json import tenants_client
from tempest.services.identity.v2.json import users_client
from tempest.services.image.v2.json import images_client
from tempest.services.network.json import network_client
from tempest.services.network.json import routers_client
from tempest.services.object_storage import container_client
from tempest.services.object_storage import object_client
from tempest.services.telemetry.json import alarming_client
from tempest.services.telemetry.json import telemetry_client
from tempest.services.volume.v1.json import volumes_client

CONF = config.CONF
OPTS = {}
USERS = {}
RES = collections.defaultdict(list)

LOG = None

JAVELIN_START = datetime.datetime.utcnow()


class OSClient(object):
    _creds = None
    identity = None
    servers = None

    def __init__(self, user, pw, tenant):
        default_params = {
            'disable_ssl_certificate_validation':
                CONF.identity.disable_ssl_certificate_validation,
            'ca_certs': CONF.identity.ca_certificates_file,
            'trace_requests': CONF.debug.trace_requests
        }
        default_params_with_timeout_values = {
            'build_interval': CONF.compute.build_interval,
            'build_timeout': CONF.compute.build_timeout
        }
        default_params_with_timeout_values.update(default_params)

        compute_params = {
            'service': CONF.compute.catalog_type,
            'region': CONF.compute.region or CONF.identity.region,
            'endpoint_type': CONF.compute.endpoint_type,
            'build_interval': CONF.compute.build_interval,
            'build_timeout': CONF.compute.build_timeout
        }
        compute_params.update(default_params)

        object_storage_params = {
            'service': CONF.object_storage.catalog_type,
            'region': CONF.object_storage.region or CONF.identity.region,
            'endpoint_type': CONF.object_storage.endpoint_type
        }
        object_storage_params.update(default_params)

        _creds = auth.KeystoneV2Credentials(
            username=user,
            password=pw,
            tenant_name=tenant)
        auth_provider_params = {
            'disable_ssl_certificate_validation':
                CONF.identity.disable_ssl_certificate_validation,
            'ca_certs': CONF.identity.ca_certificates_file,
            'trace_requests': CONF.debug.trace_requests
        }
        _auth = auth.KeystoneV2AuthProvider(
            _creds, CONF.identity.uri, **auth_provider_params)
        self.identity = identity_client.IdentityClient(
            _auth,
            CONF.identity.catalog_type,
            CONF.identity.region,
            endpoint_type='adminURL',
            **default_params_with_timeout_values)
        self.tenants = tenants_client.TenantsClient(
            _auth,
            CONF.identity.catalog_type,
            CONF.identity.region,
            endpoint_type='adminURL',
            **default_params_with_timeout_values)
        self.roles = roles_client.RolesClient(
            _auth,
            CONF.identity.catalog_type,
            CONF.identity.region,
            endpoint_type='adminURL',
            **default_params_with_timeout_values)
        self.users = users_client.UsersClient(
            _auth,
            CONF.identity.catalog_type,
            CONF.identity.region,
            endpoint_type='adminURL',
            **default_params_with_timeout_values)
        self.servers = servers_client.ServersClient(_auth,
                                                    **compute_params)
        self.flavors = flavors_client.FlavorsClient(_auth,
                                                    **compute_params)
        self.floating_ips = floating_ips_client.FloatingIPsClient(
            _auth, **compute_params)
        self.secgroups = security_groups_client.SecurityGroupsClient(
            _auth, **compute_params)
        self.secrules = security_group_rules_client.SecurityGroupRulesClient(
            _auth, **compute_params)
        self.objects = object_client.ObjectClient(_auth,
                                                  **object_storage_params)
        self.containers = container_client.ContainerClient(
            _auth, **object_storage_params)
        self.images = images_client.ImagesClientV2(
            _auth,
            CONF.image.catalog_type,
            CONF.image.region or CONF.identity.region,
            endpoint_type=CONF.image.endpoint_type,
            build_interval=CONF.image.build_interval,
            build_timeout=CONF.image.build_timeout,
            **default_params)
        self.telemetry = telemetry_client.TelemetryClient(
            _auth,
            CONF.telemetry.catalog_type,
            CONF.identity.region,
            endpoint_type=CONF.telemetry.endpoint_type,
            **default_params_with_timeout_values)
        self.alarming = alarming_client.AlarmingClient(
            _auth,
            CONF.alarm.catalog_type,
            CONF.identity.region,
            endpoint_type=CONF.alarm.endpoint_type,
            **default_params_with_timeout_values)
        self.volumes = volumes_client.VolumesClient(
            _auth,
            CONF.volume.catalog_type,
            CONF.volume.region or CONF.identity.region,
            endpoint_type=CONF.volume.endpoint_type,
            build_interval=CONF.volume.build_interval,
            build_timeout=CONF.volume.build_timeout,
            **default_params)
        self.networks = network_client.NetworkClient(
            _auth,
            CONF.network.catalog_type,
            CONF.network.region or CONF.identity.region,
            endpoint_type=CONF.network.endpoint_type,
            build_interval=CONF.network.build_interval,
            build_timeout=CONF.network.build_timeout,
            **default_params)
        self.routers = routers_client.RoutersClient(
            _auth,
            CONF.network.catalog_type,
            CONF.network.region or CONF.identity.region,
            endpoint_type=CONF.network.endpoint_type,
            build_interval=CONF.network.build_interval,
            build_timeout=CONF.network.build_timeout,
            **default_params)
        self.subnets = subnets_client.SubnetsClient(
            _auth,
            CONF.network.catalog_type,
            CONF.network.region or CONF.identity.region,
            endpoint_type=CONF.network.endpoint_type,
            build_interval=CONF.network.build_interval,
            build_timeout=CONF.network.build_timeout,
            **default_params)


def load_resources(fname):
    """Load the expected resources from a yaml file."""
    return yaml.load(open(fname, 'r'))


def keystone_admin():
    return OSClient(OPTS.os_username, OPTS.os_password, OPTS.os_tenant_name)


def client_for_user(name):
    LOG.debug("Entering client_for_user")
    if name in USERS:
        user = USERS[name]
        LOG.debug("Created client for user %s" % user)
        return OSClient(user['name'], user['pass'], user['tenant'])
    else:
        LOG.error("%s not found in USERS: %s" % (name, USERS))


###################
#
# TENANTS
#
###################


def create_tenants(tenants):
    """Create tenants from resource definition.

    Don't create the tenants if they already exist.
    """
    admin = keystone_admin()
    body = admin.tenants.list_tenants()['tenants']
    existing = [x['name'] for x in body]
    for tenant in tenants:
        if tenant not in existing:
            admin.tenants.create_tenant(tenant)['tenant']
        else:
            LOG.warning("Tenant '%s' already exists in this environment"
                        % tenant)


def destroy_tenants(tenants):
    admin = keystone_admin()
    for tenant in tenants:
        tenant_id = identity.get_tenant_by_name(admin.tenant, tenant)['id']
        admin.tenants.delete_tenant(tenant_id)

##############
#
# USERS
#
##############


def _users_for_tenant(users, tenant):
    u_for_t = []
    for user in users:
        for n in user:
            if user[n]['tenant'] == tenant:
                u_for_t.append(user[n])
    return u_for_t


def _tenants_from_users(users):
    tenants = set()
    for user in users:
        for n in user:
            tenants.add(user[n]['tenant'])
    return tenants


def _assign_swift_role(user, swift_role):
    admin = keystone_admin()
    roles = admin.roles.list_roles()
    role = next(r for r in roles if r['name'] == swift_role)
    LOG.debug(USERS[user])
    try:
        admin.roles.assign_user_role(
            USERS[user]['tenant_id'],
            USERS[user]['id'],
            role['id'])
    except lib_exc.Conflict:
        # don't care if it's already assigned
        pass


def create_users(users):
    """Create tenants from resource definition.

    Don't create the tenants if they already exist.
    """
    global USERS
    LOG.info("Creating users")
    admin = keystone_admin()
    for u in users:
        try:
            tenant = identity.get_tenant_by_name(admin.tenants, u['tenant'])
        except lib_exc.NotFound:
            LOG.error("Tenant: %s - not found" % u['tenant'])
            continue
        try:
            identity.get_user_by_username(admin.tenants,
                                          tenant['id'], u['name'])
            LOG.warning("User '%s' already exists in this environment"
                        % u['name'])
        except lib_exc.NotFound:
            admin.users.create_user(
                u['name'], u['pass'], tenant['id'],
                "%s@%s" % (u['name'], tenant['id']),
                enabled=True)


def destroy_users(users):
    admin = keystone_admin()
    for user in users:
        tenant_id = identity.get_tenant_by_name(admin.tenants,
                                                user['tenant'])['id']
        user_id = identity.get_user_by_username(admin.tenants,
                                                tenant_id, user['name'])['id']
        admin.users.delete_user(user_id)


def collect_users(users):
    global USERS
    LOG.info("Collecting users")
    admin = keystone_admin()
    for u in users:
        tenant = identity.get_tenant_by_name(admin.tenants, u['tenant'])
        u['tenant_id'] = tenant['id']
        USERS[u['name']] = u
        body = identity.get_user_by_username(admin.tenants,
                                             tenant['id'], u['name'])
        USERS[u['name']]['id'] = body['id']


class JavelinCheck(unittest.TestCase):
    def __init__(self, users, resources):
        super(JavelinCheck, self).__init__()
        self.users = users
        self.res = resources

    def runTest(self, *args):
        pass

    def _ping_ip(self, ip_addr, count, namespace=None):
        if namespace is None:
            ping_cmd = "ping -c1 " + ip_addr
        else:
            ping_cmd = "sudo ip netns exec %s ping -c1 %s" % (namespace,
                                                              ip_addr)
        for current in range(count):
            return_code = os.system(ping_cmd)
            if return_code is 0:
                break
        self.assertNotEqual(current, count - 1,
                            "Server is not pingable at %s" % ip_addr)

    def check(self):
        self.check_users()
        self.check_objects()
        self.check_servers()
        self.check_volumes()
        self.check_telemetry()
        self.check_secgroups()

        # validate neutron is enabled and ironic disabled:
        # Tenant network isolation is not supported when using ironic.
        # "admin" has set up a neutron flat network environment within a shared
        # fixed network for all tenants to use.
        # In this case, network/subnet/router creation can be skipped and the
        # server booted the same as nova network.
        if (CONF.service_available.neutron and
                not CONF.baremetal.driver_enabled):
            self.check_networking()

    def check_users(self):
        """Check that the users we expect to exist, do.

        We don't use the resource list for this because we need to validate
        that things like tenantId didn't drift across versions.
        """
        LOG.info("checking users")
        for name, user in six.iteritems(self.users):
            client = keystone_admin()
            found = client.users.show_user(user['id'])['user']
            self.assertEqual(found['name'], user['name'])
            self.assertEqual(found['tenantId'], user['tenant_id'])

            # also ensure we can auth with that user, and do something
            # on the cloud. We don't care about the results except that it
            # remains authorized.
            client = client_for_user(user['name'])
            client.servers.list_servers()

    def check_objects(self):
        """Check that the objects created are still there."""
        if not self.res.get('objects'):
            return
        LOG.info("checking objects")
        for obj in self.res['objects']:
            client = client_for_user(obj['owner'])
            r, contents = client.objects.get_object(
                obj['container'], obj['name'])
            source = _file_contents(obj['file'])
            self.assertEqual(contents, source)

    def check_servers(self):
        """Check that the servers are still up and running."""
        if not self.res.get('servers'):
            return
        LOG.info("checking servers")
        for server in self.res['servers']:
            client = client_for_user(server['owner'])
            found = _get_server_by_name(client, server['name'])
            self.assertIsNotNone(
                found,
                "Couldn't find expected server %s" % server['name'])

            found = client.servers.show_server(found['id'])['server']
            # validate neutron is enabled and ironic disabled:
            if (CONF.service_available.neutron and
                    not CONF.baremetal.driver_enabled):
                _floating_is_alive = False
                for network_name, body in found['addresses'].items():
                    for addr in body:
                        ip = addr['addr']
                        # Use floating IP, fixed IP or other type to
                        # reach the server.
                        # This is useful in multi-node environment.
                        if CONF.validation.connect_method == 'floating':
                            if addr.get('OS-EXT-IPS:type',