Пример #1
0
 def __init__(self, domain, python_version):
     self.domain = domain
     self.python_version = python_version
     self.project_path = Path(f'~/{domain}').expanduser()
     self.virtualenv = Virtualenv(self.domain, self.python_version)
     self.wsgi_file_path = Path(
         f"/var/www/{domain.replace('.', '_')}_wsgi.py")
     self.webapp = Webapp(domain)
Пример #2
0
    def test_nuke_option_overrides_all_but_token_check(self, api_token,
                                                       api_responses,
                                                       fake_home,
                                                       virtualenvs_folder):
        webapp = Webapp(self.domain)
        (fake_home / self.domain).mkdir()
        (virtualenvs_folder / self.domain).mkdir()

        webapp.sanity_checks(nuke=True)  # should not raise
Пример #3
0
def reload(domain_name: str = typer.Option(
    "your-username.pythonanywhere.com",
    "-d",
    "--domain",
    help="Domain name, eg www.mydomain.com",
)):
    domain_name = ensure_domain(domain_name)
    webapp = Webapp(domain_name)
    webapp.reload()
    typer.echo(snakesay(f"{domain_name} has been reloaded"))
Пример #4
0
    def test_returns_json_from_server_having_parsed_expiry_with_timezone_offset_and_separators(
            self, api_responses, api_token):
        expected_url = (get_api_endpoint().format(username=getpass.getuser(),
                                                  flavor="webapps") +
                        "mydomain.com/ssl/")
        api_responses.add(
            responses.GET,
            expected_url,
            status=200,
            body=json.dumps({
                "not_after":
                "2018-08-24T17:16:23+00:00",
                "issuer_name":
                "PythonAnywhere test CA",
                "subject_name":
                "www.mycoolsite.com",
                "subject_alternate_names":
                ["www.mycoolsite.com", "mycoolsite.com"],
            }),
        )

        assert Webapp("mydomain.com").get_ssl_info() == {
            "not_after": datetime(2018, 8, 24, 17, 16, 23, tzinfo=tzutc()),
            "issuer_name": "PythonAnywhere test CA",
            "subject_name": "www.mycoolsite.com",
            "subject_alternate_names":
            ["www.mycoolsite.com", "mycoolsite.com"],
        }

        get = api_responses.calls[0]
        assert get.request.method == "GET"
        assert get.request.url == expected_url
        assert get.request.headers["Authorization"] == f"Token {api_token}"
Пример #5
0
    def test_raises_if_patch_does_not_20x(self, api_responses, api_token):
        expected_post_url = get_api_endpoint().format(
            username=getpass.getuser(), flavor="webapps")
        expected_patch_url = (get_api_endpoint().format(
            username=getpass.getuser(), flavor="webapps") + "mydomain.com/")
        api_responses.add(
            responses.POST,
            expected_post_url,
            status=201,
            body=json.dumps({"status": "OK"}),
        )
        api_responses.add(
            responses.PATCH,
            expected_patch_url,
            status=400,
            json={"message": "an error"},
        )

        with pytest.raises(Exception) as e:
            Webapp("mydomain.com").create("3.7",
                                          "/virtualenv/path",
                                          "/project/path",
                                          nuke=False)

        assert (
            "PATCH to set virtualenv path and source directory via API failed"
            in str(e.value))
        assert "an error" in str(e.value)
Пример #6
0
    def test_does_patch_to_update_virtualenv_path_and_source_directory(
            self, api_responses, api_token):
        expected_post_url = get_api_endpoint().format(
            username=getpass.getuser(), flavor="webapps")
        expected_patch_url = (get_api_endpoint().format(
            username=getpass.getuser(), flavor="webapps") + "mydomain.com/")
        api_responses.add(
            responses.POST,
            expected_post_url,
            status=201,
            body=json.dumps({"status": "OK"}),
        )
        api_responses.add(responses.PATCH, expected_patch_url, status=200)

        Webapp("mydomain.com").create("3.7",
                                      "/virtualenv/path",
                                      "/project/path",
                                      nuke=False)

        patch = api_responses.calls[1]
        assert patch.request.url == expected_patch_url
        assert patch.request.body == urlencode({
            "virtualenv_path":
            "/virtualenv/path",
            "source_directory":
            "/project/path"
        })
        assert patch.request.headers["Authorization"] == f"Token {api_token}"
Пример #7
0
    def test_does_post_to_create_webapp(self, api_responses, api_token):
        expected_post_url = get_api_endpoint().format(
            username=getpass.getuser(), flavor="webapps")
        expected_patch_url = (get_api_endpoint().format(
            username=getpass.getuser(), flavor="webapps") + "mydomain.com/")
        api_responses.add(
            responses.POST,
            expected_post_url,
            status=201,
            body=json.dumps({"status": "OK"}),
        )
        api_responses.add(responses.PATCH, expected_patch_url, status=200)

        Webapp("mydomain.com").create("3.7",
                                      "/virtualenv/path",
                                      "/project/path",
                                      nuke=False)

        post = api_responses.calls[0]
        assert post.request.url == expected_post_url
        assert post.request.body == urlencode({
            "domain_name":
            "mydomain.com",
            "python_version":
            PYTHON_VERSIONS["3.7"]
        })
        assert post.request.headers["Authorization"] == f"Token {api_token}"
Пример #8
0
    def test_get_list_of_logs(self, api_responses, api_token):
        expected_url = (get_api_endpoint().format(username=getpass.getuser(),
                                                  flavor="files") +
                        "tree/?path=/var/log/")
        api_responses.add(
            responses.GET,
            expected_url,
            status=200,
            body=json.dumps([
                "/var/log/blah",
                "/var/log/mydomain.com.access.log",
                "/var/log/mydomain.com.access.log.1",
                "/var/log/mydomain.com.access.log.2.gz",
                "/var/log/mydomain.com.error.log",
                "/var/log/mydomain.com.error.log.1",
                "/var/log/mydomain.com.error.log.2.gz",
                "/var/log/mydomain.com.server.log",
                "/var/log/mydomain.com.server.log.1",
                "/var/log/mydomain.com.server.log.2.gz",
            ]),
        )

        logs = Webapp("mydomain.com").get_log_info()

        post = api_responses.calls[0]
        assert post.request.url == expected_url
        assert post.request.headers["Authorization"] == f"Token {api_token}"
        assert logs == {
            "access": [0, 1, 2],
            "error": [0, 1, 2],
            "server": [0, 1, 2]
        }
Пример #9
0
    def test_does_two_posts_to_static_files_endpoint(self, api_token,
                                                     api_responses):
        expected_url = (get_api_endpoint().format(username=getpass.getuser(),
                                                  flavor="webapps") +
                        "mydomain.com/static_files/")
        api_responses.add(responses.POST, expected_url, status=201)
        api_responses.add(responses.POST, expected_url, status=201)

        Webapp("mydomain.com").add_default_static_files_mappings(
            "/project/path")

        post1 = api_responses.calls[0]
        assert post1.request.url == expected_url
        assert post1.request.headers["content-type"] == "application/json"
        assert post1.request.headers["Authorization"] == f"Token {api_token}"
        assert json.loads(post1.request.body.decode("utf8")) == {
            "url": "/static/",
            "path": "/project/path/static",
        }
        post2 = api_responses.calls[1]
        assert post2.request.url == expected_url
        assert post2.request.headers["content-type"] == "application/json"
        assert post2.request.headers["Authorization"] == f"Token {api_token}"
        assert json.loads(post2.request.body.decode("utf8")) == {
            "url": "/media/",
            "path": "/project/path/media",
        }
Пример #10
0
    def test_raises_if_webapp_already_exists(self, api_token, api_responses):
        webapp = Webapp(self.domain)
        api_responses.add(
            responses.GET,
            self.expected_url,
            status=200,
            body=json.dumps({
                "id": 1,
                "domain_name": self.domain
            }),
        )

        with pytest.raises(SanityException) as e:
            webapp.sanity_checks(nuke=False)

        assert "You already have a webapp for " + self.domain in str(e.value)
        assert "nuke" in str(e.value)
Пример #11
0
def main(domain_name, certificate_file, private_key_file, suppress_reload):
    if not os.path.exists(certificate_file):
        print(f"Could not find certificate file {certificate_file}")
        sys.exit(1)
    with open(certificate_file, "r") as f:
        certificate = f.read()

    if not os.path.exists(private_key_file):
        print(f"Could not find private key file {private_key_file}")
        sys.exit(1)
    with open(private_key_file, "r") as f:
        private_key = f.read()

    webapp = Webapp(domain_name)
    webapp.set_ssl(certificate, private_key)
    if not suppress_reload:
        webapp.reload()

    ssl_details = webapp.get_ssl_info()
    print(snakesay(
        "That's all set up now :-)\n"
        "Your new certificate will expire on {expiry:%d %B %Y},\n"
        "so shortly before then you should renew it\n"
        "and install the new certificate.".format(
            expiry=ssl_details["not_after"]
        )
    ))
Пример #12
0
    def test_raises_if_get_does_not_20x(self, api_responses, api_token):
        expected_url = (get_api_endpoint().format(username=getpass.getuser(),
                                                  flavor="files") +
                        "tree/?path=/var/log/")
        api_responses.add(responses.GET, expected_url, status=404, body="nope")

        with pytest.raises(Exception) as e:
            Webapp("mydomain.com").get_log_info()

        assert "GET log files info via API failed" in str(e.value)
        assert "nope" in str(e.value)
Пример #13
0
    def test_raises_if_get_does_not_return_200(self, api_responses, api_token):
        expected_url = (get_api_endpoint().format(username=getpass.getuser(),
                                                  flavor="webapps") +
                        "mydomain.com/ssl/")
        api_responses.add(responses.GET, expected_url, status=404, body="nope")

        with pytest.raises(Exception) as e:
            Webapp("mydomain.com").get_ssl_info()

        assert "GET SSL details via API failed, got" in str(e.value)
        assert "nope" in str(e.value)
Пример #14
0
    def test_delete_old_access_log(self, api_responses, api_token):
        expected_url = (get_api_endpoint().format(username=getpass.getuser(),
                                                  flavor="files") +
                        "path/var/log/mydomain.com.access.log.1/")
        api_responses.add(responses.DELETE, expected_url, status=200)

        Webapp("mydomain.com").delete_log(log_type="access", index=1)

        post = api_responses.calls[0]
        assert post.request.url == expected_url
        assert post.request.body is None
        assert post.request.headers["Authorization"] == f"Token {api_token}"
Пример #15
0
    def test_does_post_to_reload_url(self, api_responses, api_token):
        expected_url = (get_api_endpoint().format(username=getpass.getuser(),
                                                  flavor="webapps") +
                        "mydomain.com/reload/")
        api_responses.add(responses.POST, expected_url, status=200)

        Webapp("mydomain.com").reload()

        post = api_responses.calls[0]
        assert post.request.url == expected_url
        assert post.request.body is None
        assert post.request.headers["Authorization"] == f"Token {api_token}"
Пример #16
0
    def test_raises_if_delete_does_not_20x(self, api_responses, api_token):
        expected_url = (get_api_endpoint().format(username=getpass.getuser(),
                                                  flavor="files") +
                        "path/var/log/mydomain.com.access.log/")
        api_responses.add(responses.DELETE,
                          expected_url,
                          status=404,
                          body="nope")

        with pytest.raises(Exception) as e:
            Webapp("mydomain.com").delete_log(log_type="access")

        assert "DELETE log file via API failed" in str(e.value)
        assert "nope" in str(e.value)
Пример #17
0
    def test_raises_if_post_does_not_20x(self, api_responses, api_token):
        expected_url = (get_api_endpoint().format(username=getpass.getuser(),
                                                  flavor="webapps") +
                        "mydomain.com/ssl/")
        api_responses.add(responses.POST,
                          expected_url,
                          status=404,
                          body="nope")

        with pytest.raises(Exception) as e:
            Webapp("mydomain.com").set_ssl("foo", "bar")

        assert "POST to set SSL details via API failed" in str(e.value)
        assert "nope" in str(e.value)
Пример #18
0
    def test_raises_if_post_does_not_20x_that_is_not_a_cname_error(
            self, api_responses, api_token):
        expected_url = (get_api_endpoint().format(username=getpass.getuser(),
                                                  flavor="webapps") +
                        "mydomain.com/reload/")
        api_responses.add(responses.POST,
                          expected_url,
                          status=404,
                          body="nope")

        with pytest.raises(Exception) as e:
            Webapp("mydomain.com").reload()

        assert "POST to reload webapp via API failed" in str(e.value)
        assert "nope" in str(e.value)
Пример #19
0
    def test_raises_if_post_does_not_20x(self, api_responses, api_token):
        expected_post_url = get_api_endpoint().format(
            username=getpass.getuser(), flavor="webapps")
        api_responses.add(responses.POST,
                          expected_post_url,
                          status=500,
                          body="an error")

        with pytest.raises(Exception) as e:
            Webapp("mydomain.com").create("3.7",
                                          "/virtualenv/path",
                                          "/project/path",
                                          nuke=False)

        assert "POST to create webapp via API failed" in str(e.value)
        assert "an error" in str(e.value)
Пример #20
0
def install_ssl(
    domain_name: str = typer.Argument(
        ...,
        help="Domain name, eg www.mydomain.com",
    ),
    certificate_file: Path = typer.Argument(
        ...,
        exists=True,
        file_okay=True,
        readable=True,
        resolve_path=True,
        help=
        "The name of the file containing the combined certificate in PEM format (normally a number of blocks, "
        'each one starting "BEGIN CERTIFICATE" and ending "END CERTIFICATE")',
    ),
    private_key_file: Path = typer.Argument(
        ...,
        exists=True,
        file_okay=True,
        readable=True,
        resolve_path=True,
        help=
        "The name of the file containing the private key in PEM format (a file with one block, "
        'starting with something like "BEGIN PRIVATE KEY" and ending with something like "END PRIVATE KEY")',
    ),
    suppress_reload: bool = typer.Option(
        False,
        help=
        "The website will need to be reloaded in order to activate the new certificate/key combination "
        "-- this happens by default, use this option to suppress it.",
    ),
):
    with open(certificate_file, "r") as f:
        certificate = f.read()

    with open(private_key_file, "r") as f:
        private_key = f.read()

    webapp = Webapp(domain_name)
    webapp.set_ssl(certificate, private_key)
    if not suppress_reload:
        webapp.reload()

    ssl_details = webapp.get_ssl_info()
    typer.echo(
        snakesay("That's all set up now :-)\n"
                 f"Your new certificate for {domain_name} will expire\n"
                 f"on {ssl_details['not_after'].date().isoformat()},\n"
                 "so shortly before then you should renew it\n"
                 "and install the new certificate."))
Пример #21
0
    def test_does_not_raise_if_post_responds_with_a_cname_error(
            self, api_responses, api_token):
        expected_url = (get_api_endpoint().format(username=getpass.getuser(),
                                                  flavor="webapps") +
                        "mydomain.com/reload/")
        api_responses.add(
            responses.POST,
            expected_url,
            status=409,
            json={
                "status": "error",
                "error": "cname_error"
            },
        )

        ## Should not raise
        Webapp("mydomain.com").reload()
Пример #22
0
    def test_ignores_404_from_delete_call_when_nuking(self, api_responses,
                                                      api_token):
        post_url = get_api_endpoint().format(username=getpass.getuser(),
                                             flavor="webapps")
        webapp_url = (get_api_endpoint().format(
            username=getpass.getuser(), flavor="webapps") + "mydomain.com/")
        api_responses.add(responses.DELETE, webapp_url, status=404)
        api_responses.add(responses.POST,
                          post_url,
                          status=201,
                          body=json.dumps({"status": "OK"}))
        api_responses.add(responses.PATCH, webapp_url, status=200)

        Webapp("mydomain.com").create("3.7",
                                      "/virtualenv/path",
                                      "/project/path",
                                      nuke=True)
Пример #23
0
    def test_does_post_to_ssl_url(self, api_responses, api_token):
        expected_url = (get_api_endpoint().format(username=getpass.getuser(),
                                                  flavor="webapps") +
                        "mydomain.com/ssl/")
        api_responses.add(responses.POST, expected_url, status=200)
        certificate = "certificate data"
        private_key = "private key data"

        Webapp("mydomain.com").set_ssl(certificate, private_key)

        post = api_responses.calls[0]
        assert post.request.url == expected_url
        assert json.loads(post.request.body.decode("utf8")) == {
            "private_key": "private key data",
            "cert": "certificate data",
        }
        assert post.request.headers["Authorization"] == f"Token {api_token}"
def main(domain_name, suppress_reload):
    homedir = expanduser("~")
    possible_paths = (
        os.path.join(homedir, 'letsencrypt', domain_name),
        os.path.join(homedir, 'letsencrypt', 'certs', domain_name),
    )
    done = False
    for path in possible_paths:
        certificate_file = os.path.join(path, 'fullchain.pem')
        private_key_file = os.path.join(path, 'privkey.pem')
        if os.path.exists(certificate_file) and os.path.exists(private_key_file):
            with open(certificate_file, "r") as f:
                certificate = f.read()
            with open(private_key_file, "r") as f:
                private_key = f.read()

            webapp = Webapp(domain_name)
            webapp.set_ssl(certificate, private_key)
            if not suppress_reload:
                webapp.reload()

            ssl_details = webapp.get_ssl_info()
            print(
                snakesay(
                    "This method of handling Let's Encrypt certs\n"
                    "**************IS DEPRECATED.**************\n\n"
                    "You can now have a Let's Encrypt certificate managed by PythonAnywhere.\n"
                    "We handle all the details of getting it, installing it,\n"
                    "and managing renewals for you. So you don't need to do\n"
                    "any of the stuff below any more.\n\n"
                    "Anyway, All is set up for now. \n"
                    "Your new certificate will expire on {expiry:%d %B %Y},\n"
                    "so shortly before then you should switch to the new system\n"
                    "(see https://help.pythonanywhere.com/pages/HTTPSSetup/)\n"
                    "".format(
                        expiry=ssl_details["not_after"]
                    )
                )
            )

            done = True
            break

    if not done:
        print("Could not find certificate or key files (looked in {possible_paths})".format(
            possible_paths=possible_paths
        ))
        sys.exit(2)
Пример #25
0
    def test_does_delete_first_for_nuke_call(self, api_responses, api_token):
        post_url = get_api_endpoint().format(username=getpass.getuser(),
                                             flavor="webapps")
        webapp_url = (get_api_endpoint().format(
            username=getpass.getuser(), flavor="webapps") + "mydomain.com/")
        api_responses.add(responses.DELETE, webapp_url, status=200)
        api_responses.add(responses.POST,
                          post_url,
                          status=201,
                          body=json.dumps({"status": "OK"}))
        api_responses.add(responses.PATCH, webapp_url, status=200)

        Webapp("mydomain.com").create("3.7",
                                      "/virtualenv/path",
                                      "/project/path",
                                      nuke=True)

        delete = api_responses.calls[0]
        assert delete.request.method == "DELETE"
        assert delete.request.url == webapp_url
        assert delete.request.headers["Authorization"] == f"Token {api_token}"
Пример #26
0
    def test_raises_if_post_returns_a_200_with_status_error(
            self, api_responses, api_token):
        expected_post_url = get_api_endpoint().format(
            username=getpass.getuser(), flavor="webapps")
        api_responses.add(
            responses.POST,
            expected_post_url,
            status=200,
            body=json.dumps({
                "status": "ERROR",
                "error_type": "bad",
                "error_message": "bad things happened",
            }),
        )

        with pytest.raises(Exception) as e:
            Webapp("mydomain.com").create("3.7",
                                          "/virtualenv/path",
                                          "/project/path",
                                          nuke=False)

        assert "POST to create webapp via API failed" in str(e.value)
        assert "bad things happened" in str(e.value)
Пример #27
0
class Project:
    def __init__(self, domain, python_version):
        self.domain = domain
        self.python_version = python_version
        self.project_path = Path(f'~/{domain}').expanduser()
        self.virtualenv = Virtualenv(self.domain, self.python_version)
        self.wsgi_file_path = Path(
            f"/var/www/{domain.replace('.', '_')}_wsgi.py")
        self.webapp = Webapp(domain)

    def sanity_checks(self, nuke):
        self.webapp.sanity_checks(nuke=nuke)
        if nuke:
            return
        if self.virtualenv.path.exists():
            raise SanityException(
                "You already have a virtualenv for {domain}.\n\n"
                "Use the --nuke option if you want to replace it.".format(
                    domain=self.domain))
        if self.project_path.exists():
            raise SanityException(
                "You already have a project folder at {project_path}.\n\n"
                "Use the --nuke option if you want to replace it.".format(
                    project_path=self.project_path))

    def create_webapp(self, nuke):
        self.webapp.create(self.python_version,
                           self.virtualenv.path,
                           self.project_path,
                           nuke=nuke)

    def add_static_file_mappings(self):
        self.webapp.add_default_static_files_mappings(self.project_path)

    def start_bash(self):
        print(
            snakesay(
                'Starting Bash shell with activated virtualenv in project directory.  Press Ctrl+D to exit.'
            ))
        unique_id = str(uuid.uuid4())
        launch_bash_in_virtualenv(self.virtualenv.path, unique_id,
                                  self.project_path)
Пример #28
0
def main(domain_name):
    webapp = Webapp(domain_name)
    webapp.reload()
    print(snakesay(f"{domain_name} has been reloaded"))
Пример #29
0
def main(domain, log_type, log_index):
    webapp = Webapp(ensure_domain(domain))
    log_types = ["access", "error", "server"]
    logs = webapp.get_log_info()
    if log_type == "all" and log_index == "all":
        for key in log_types:
            for log in logs[key]:
                webapp.delete_log(key, log)
    elif log_type == "all":
        for key in log_types:
            webapp.delete_log(key, int(log_index))
    elif log_index == "all":
        for i in logs[log_type]:
            webapp.delete_log(log_type, int(i))
    else:
        webapp.delete_log(log_type, int(log_index))
    print(snakesay('All Done!'))
Пример #30
0
 def test_does_not_raise_if_no_webapp(self, api_token, api_responses):
     webapp = Webapp(self.domain)
     api_responses.add(responses.GET, self.expected_url, status=404)
     webapp.sanity_checks(nuke=False)  # should not raise