<commit_before># This file is part of the Juju GUI, which lets users view and manage Juju
# environments within a graphical interface (https://launchpad.net/juju-gui).
# Copyright (C) 2013 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License version 3, as published by
# the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""Bundle deployment base objects.

This module defines the base pieces of the bundle support infrastructure,
including the Deployer object, responsible of starting/scheduling deployments,
and the DeployMiddleware, glue code that connects the WebSocket handler, the
bundle views and the Deployer itself. See the bundles package docstring for
a detailed explanation of how these objects are used.
"""

import time

from concurrent.futures import (
    process,
    ProcessPoolExecutor,
)
from tornado import gen
from tornado.ioloop import IOLoop
from tornado.util import ObjectDict

from guiserver.bundles import (
    blocking,
    utils,
    views,
)
from guiserver.utils import add_future
from guiserver.watchers import WatcherError


# Controls how many more calls than processes will be queued in the call queue.
# Set to zero to make Future.cancel() succeed more frequently (Futures in the
# call queue cannot be cancelled).
process.EXTRA_QUEUED_CALLS = 0
# Juju API versions supported by the GUI server Deployer.
# Tests use the first API version in this list.
SUPPORTED_API_VERSIONS = ['go']


class Deployer(object):
    """Handle the bundle deployment process.

    This class provides the logic to validate deployment requests based on the
    current state of the Juju environment, and to start/observe the import
    process.

    The validation and deployments steps are executed in separate processes.
    It is possible to process only one bundle at the time.

    Note that the Deployer is not intended to store request related state: it
    is instantiated once when the application is bootstrapped and used as a
    singleton by all WebSocket requests.
    """

    def __init__(self, apiurl, apiversion, io_loop=None):
        """Initialize the deployer.

        The apiurl argument is the URL of the juju-core WebSocket server.
        The apiversion argument is the Juju API version (e.g. "go").
        """
        self._apiurl = apiurl
        self._apiversion = apiversion
        if io_loop is None:
            io_loop = IOLoop.current()
        self._io_loop = io_loop

        # Deployment validation and importing executors.
        self._validate_executor = ProcessPoolExecutor(1)
        self._run_executor = ProcessPoolExecutor(1)

        # An observer instance is used to watch the deployments progress.
        self._observer = utils.Observer()
        # Queue stores the deployment identifiers corresponding to the
        # currently started/queued jobs.
        self._queue = []
        # The futures attribute maps deployment identifiers to Futures.
        self._futures = {}

    @gen.coroutine
    def validate(self, user, name, bundle):
        """Validate the deployment bundle.

        The validation is executed in a separate process using the
        juju-deployer library.

        Three arguments are provided:
          - user: the current authenticated user;
          - name: then name of the bundle to be imported;
          - bundle: a YAML decoded object representing the bundle contents.

        Return a Future whose result is a string representing an error or None
        if no error occurred.
        """
        apiversion = self._apiversion
        if apiversion not in SUPPORTED_API_VERSIONS:
            raise gen.Return('unsupported API version: {}'.format(apiversion))
        try:
            yield self._validate_executor.submit(
                blocking.validate, self._apiurl, user.password, bundle)
        except Exception as err:
            raise gen.Return(str(err))

    def import_bundle(self, user, name, bundle, test_callback=None):
        """Schedule a deployment bundle import process.

        The deployment is executed in a separate process.

        Three arguments are required:
          - user: the current authenticated user;
          - name: then name of the bundle to be imported;
          - bundle: a YAML decoded object representing the bundle contents.

        It is possible to also provide an optional test_callback that will be
        called when the deployment is completed. Note that this functionality
        is present only for tests: clients should not consider the
        test_callback argument part of the API, and should instead use the
        watch/next methods to observe the progress of a deployment (see below).

        Return the deployment identifier assigned to this deployment process.
        """
        # Start observing this deployment, retrieve the next available
        # deployment id and notify its position at the end of the queue.
        deployment_id = self._observer.add_deployment()
        self._observer.notify_position(deployment_id, len(self._queue))
        # Add this deployment to the queue.
        self._queue.append(deployment_id)
        # Add the import bundle job to the run executor, and set up a callback
        # to be called when the import process completes.
        future = self._run_executor.submit(
            blocking.import_bundle, self._apiurl, user.password, name, bundle)
        add_future(self._io_loop, future, self._import_callback, deployment_id)
        self._futures[deployment_id] = future
        # If a customized callback is provided, schedule it as well.
        if test_callback is not None:
            add_future(self._io_loop, future, test_callback)
        # Submit a sleeping job in order to avoid the next deployment job to be
        # immediately put in the executor's call queue. This allows for
        # cancelling scheduled jobs, even if the job is the next to be started.
        self._run_executor.submit(time.sleep, 1)
        return deployment_id

    def _import_callback(self, deployment_id, future):
        """Callback called when a deployment process is completed.<commit_msg>Update the bundle base module.<commit_after># This file is part of the Juju GUI, which lets users view and manage Juju
# environments within a graphical interface (https://launchpad.net/juju-gui).
# Copyright (C) 2013 Canonical Ltd.
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License version 3, as published by
# the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranties of MERCHANTABILITY,
# SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

"""Bundle deployment base objects.

This module defines the base pieces of the bundle support infrastructure,
including the Deployer object, responsible of starting/scheduling deployments,
and the DeployMiddleware, glue code that connects the WebSocket handler, the
bundle views and the Deployer itself. See the bundles package docstring for
a detailed explanation of how these objects are used.
"""

import time

from concurrent.futures import (
    process,
    ProcessPoolExecutor,
)
from deployer import guiserver as blocking
from tornado import gen
from tornado.ioloop import IOLoop
from tornado.util import ObjectDict

from guiserver.bundles import (
    utils,
    views,
)
from guiserver.utils import add_future
from guiserver.watchers import WatcherError


# Controls how many more calls than processes will be queued in the call queue.
# Set to zero to make Future.cancel() succeed more frequently (Futures in the
# call queue cannot be cancelled).
process.EXTRA_QUEUED_CALLS = 0
# Juju API versions supported by the GUI server Deployer.
# Tests use the first API version in this list.
SUPPORTED_API_VERSIONS = ['go']
# Options used by the juju-deployer Importer instance.
IMPORTER_OPTIONS = ObjectDict(
    branch_only=False,  # Avoid just updating VCS branches and exiting.
    deploy_delay=0,  # Do not sleep between 'deploy' commands.
    no_local_mods=True,  # Disallow deployment of locally-modified charms.
    overrides=None,  # Do not override config options.
    rel_wait=60,  # Wait for 1 minute before checking for relation errors.
    retry_count=0,  # Do not retry on unit errors.
    timeout=45*60,  # Set a 45 minutes timeout for the entire deployment.
    update_charms=False,  # Do not update existing charm branches.
    watch=False,  # Do not watch environment changes on console.
)


class Deployer(object):
    """Handle the bundle deployment process.

    This class provides the logic to validate deployment requests based on the
    current state of the Juju environment, and to start/observe the import
    process.

    The validation and deployments steps are executed in separate processes.
    It is possible to process only one bundle at the time.

    Note that the Deployer is not intended to store request related state: it
    is instantiated once when the application is bootstrapped and used as a
    singleton by all WebSocket requests.
    """

    def __init__(self, apiurl, apiversion, io_loop=None):
        """Initialize the deployer.

        The apiurl argument is the URL of the juju-core WebSocket server.
        The apiversion argument is the Juju API version (e.g. "go").
        """
        self._apiurl = apiurl
        self._apiversion = apiversion
        if io_loop is None:
            io_loop = IOLoop.current()
        self._io_loop = io_loop

        # Deployment validation and importing executors.
        self._validate_executor = ProcessPoolExecutor(1)
        self._run_executor = ProcessPoolExecutor(1)

        # An observer instance is used to watch the deployments progress.
        self._observer = utils.Observer()
        # Queue stores the deployment identifiers corresponding to the
        # currently started/queued jobs.
        self._queue = []
        # The futures attribute maps deployment identifiers to Futures.
        self._futures = {}

    @gen.coroutine
    def validate(self, user, name, bundle):
        """Validate the deployment bundle.

        The validation is executed in a separate process using the
        juju-deployer library.

        Three arguments are provided:
          - user: the current authenticated user;
          - name: then name of the bundle to be imported;
          - bundle: a YAML decoded object representing the bundle contents.

        Return a Future whose result is a string representing an error or None
        if no error occurred.
        """
        apiversion = self._apiversion
        if apiversion not in SUPPORTED_API_VERSIONS:
            raise gen.Return('unsupported API version: {}'.format(apiversion))
        try:
            yield self._validate_executor.submit(
                blocking.validate, self._apiurl, user.password, bundle)
        except Exception as err:
            raise gen.Return(str(err))

    def import_bundle(self, user, name, bundle, test_callback=None):
        """Schedule a deployment bundle import process.

        The deployment is executed in a separate process.

        Three arguments are required:
          - user: the current authenticated user;
          - name: then name of the bundle to be imported;
          - bundle: a YAML decoded object representing the bundle contents.

        It is possible to also provide an optional test_callback that will be
        called when the deployment is completed. Note that this functionality
        is present only for tests: clients should not consider the
        test_callback argument part of the API, and should instead use the
        watch/next methods to observe the progress of a deployment (see below).

        Return the deployment identifier assigned to this deployment process.
        """
        # Start observing this deployment, retrieve the next available
        # deployment id and notify its position at the end of the queue.
        deployment_id = self._observer.add_deployment()
        self._observer.notify_position(deployment_id, len(self._queue))
        # Add this deployment to the queue.
        self._queue.append(deployment_id)
        # Add the import bundle job to the run executor, and set up a callback
        # to be called when the import process completes.
        future = self._run_executor.submit(
            blocking.import_bundle,
            self._apiurl, user.password, name, bundle, IMPORTER_OPTIONS)
        add_future(self._io_loop, future, self._import_callback, deployment_id)
        self._futures[deployment_id] = future
        # If a customized callback is provided, schedule it as well.
        if test_callback is not None:
            add_future(self._io_loop, future, test_callback)
        # Submit a sleeping job in order to avoid the next deployment job to be
        # immediately put in the executor's call queue. This allows for
        # cancelling scheduled jobs, even if the job is the next to be started.
        self._run_executor.submit(time.sleep, 1)
        return deployment_id

    def _import_callback(self, deployment_id, future):
        """Callback called when a deployment process is completed.
{'fraction_non_alphanumeric': 0.05491515715615379, 'fraction_numerical': 0.0020763227607932984, 'mean_word_length': 2.9965665236051504, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 5, 'https://': 2, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2992793', 'n_tokens_mistral': 3564, 'n_tokens_neox': 3429, 'n_words': 1617}