Я создал несколько функций Azure с помощью Python (3.9), и у меня возникли проблемы с настройкой CI/CD в репозитории GitLab, где находится код. Этот репозиторий находится на виртуальной машине Azure, не уверен, что это актуально. Функции подключаются к базе данных Azure PostgreSQL, как вы увидите далее в коде.
.gitlab-ci.yml запускает 3 задания: 2 первых создают и развертывают React. app в статические веб-приложения Azure. Третий успешно работает в конвейерах GitLab, но после этого я не вижу никаких функций на странице обзора приложения-функции Azure. Я также не вижу журналов ошибок в задании.
При использовании расширения VSCode основных инструментов функций Azure для развертывания непосредственно в приложении-функции Azure развертывание проходит успешно, и функции отображаются на странице обзора, и они работают правильно, подключаются к базе данных и все такое. Я также могу запускать и тестировать функции локально.
Вот различные фрагменты кода. Некоторая информация будет опущена в целях конфиденциальности, например URL-адреса
.gitlab-ci.yml:
stages:
- build-react
- deploy-react
- deploy-functions
# Build the React app
build-react:
stage: build-react
image: registry.gitlab.com/static-web-apps/azure-static-web-apps-deploy
script:
- cd app-frontend
- npm install
- VITE_FUNCTIONS_BASE_URL=azurewebsites-url npx vite build
artifacts:
paths:
- app-frontend/dist
only:
- main
# Deploy the React app to Azure Static Web Apps
deploy-react:
stage: deploy-react
image: registry.gitlab.com/static-web-apps/azure-static-web-apps-deploy
script:
- cd app-frontend
- npm install -g @azure/static-web-apps-cli
- swa deploy --env production --deployment-token token --output-location ./dist
only:
- main
deploy-functions:
stage: deploy-functions
image: mcr.microsoft.com/azure-functions/python:3.0 # Azure Functions image
script:
- set -e
- cd azure_functions # Navigate to the folder containing your Azure Function app
- zip -r ../my-functions.zip . # Create a zip file of only the contents of the azure_functions folder
- cd .. # Navigate back to the root directory
- echo "Zipped successfully"
- apt-get update && apt-get install -y ca-certificates curl apt-transport-https lsb-release gnupg
- curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
- echo "Deploying to Azure..."
- az login --service-principal -u $AZURE_CLIENT_ID -p $AZURE_CLIENT_SECRET --tenant $AZURE_TENANT_ID
- az functionapp deployment source config-zip --resource-group $AZURE_RESOURCE_GROUP --name $AZURE_FUNCTIONAPP_NAME --src ./my-functions.zip
only:
- main
requirements.txt:
azure-functions
psycopg2-binary
function_app.py:
import json
import azure.functions as func
import logging
from datetime import datetime
import psycopg2
import os
# Database connection details
DB_HOST = os.getenv("DB_HOST")
DB_NAME = os.getenv("DB_NAME")
DB_USER = os.getenv("DB_USER")
DB_PASSWORD = os.getenv("DB_PASSWORD")
DB_PORT = os.getenv("DB_PORT")
app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
@app.route(route="desks", methods=["GET"], auth_level=func.AuthLevel.ANONYMOUS)
def getDesks(req: func.HttpRequest) -> func.HttpResponse:
# Connect to database and fetch reservations for the given date
try:
connection = psycopg2.connect(
host=DB_HOST,
port=DB_PORT,
database=DB_NAME,
user=DB_USER,
password=DB_PASSWORD,
)
cursor = connection.cursor()
# Query to get all desks
query = """
SELECT d.id
FROM desks d;
"""
cursor.execute(query)
desks = cursor.fetchall()
# Prepare the response data
if desks:
response_data = [{"id": desk[0]} for desk in desks]
return func.HttpResponse(
body=json.dumps(response_data),
status_code=200,
mimetype="application/json",
)
else:
return func.HttpResponse("No desks found", status_code=200)
except Exception as e:
logging.error(f"Error fetching desks: {str(e)}")
return func.HttpResponse(
"An error occurred while processing the request.", status_code=500
)
finally:
if cursor:
cursor.close()
if connection:
connection.close()
@app.route(route="reservations", methods=["GET"], auth_level=func.AuthLevel.ANONYMOUS)
def getReservationsForDate(req: func.HttpRequest) -> func.HttpResponse:
# Get the date from the query parameter (expected format: YYYY-MM-DD)
reservation_date = req.params.get("date")
if not reservation_date:
return func.HttpResponse(
"Please provide a 'date' query parameter (YYYY-MM-DD).", status_code=400
)
try:
# Convert string date to a date object for validation
reservation_date = datetime.strptime(reservation_date, "%Y-%m-%d").date()
except ValueError:
return func.HttpResponse(
"Invalid date format. Please provide a valid date in YYYY-MM-DD format.",
status_code=400,
)
# Connect to database and fetch reservations for the given date
try:
connection = psycopg2.connect(
host=DB_HOST,
port=DB_PORT,
database=DB_NAME,
user=DB_USER,
password=DB_PASSWORD,
)
cursor = connection.cursor()
# Query to get all reservations for the provided date
query = """
SELECT r.id, r.desk_id, r.reserved_by, r.reservation_date
FROM reservations r
WHERE r.reservation_date = %s;
"""
cursor.execute(query, (reservation_date,))
reservations = cursor.fetchall()
# Prepare the response data
if reservations:
response_data = [
{
"id": res[0],
"desk_id": res[1],
"reserved_by": res[2],
"reservation_date": res[3].strftime("%Y-%m-%d"),
}
for res in reservations
]
return func.HttpResponse(
body=json.dumps(response_data),
status_code=200,
mimetype="application/json",
)
else:
return func.HttpResponse(body=json.dumps([]), status_code=200)
except Exception as e:
logging.error(f"Error fetching reservations: {str(e)}")
return func.HttpResponse(
"An error occurred while processing the request.", status_code=500
)
finally:
if cursor:
cursor.close()
if connection:
connection.close()
@app.route(
route="reservation-dates", methods=["GET"], auth_level=func.AuthLevel.ANONYMOUS
)
def getDatesWithReservationForEmail(req: func.HttpRequest) -> func.HttpResponse:
email = req.params.get("email")
if not email:
return func.HttpResponse(
"Please provide an 'email' query parameter.", status_code=400
)
todays_date = datetime.today().date()
# Connect to database and fetch dates in which the email provided has reservations
try:
connection = psycopg2.connect(
host=DB_HOST,
port=DB_PORT,
database=DB_NAME,
user=DB_USER,
password=DB_PASSWORD,
)
cursor = connection.cursor()
# Query to get all reservations for the provided date
query = """
SELECT r.reservation_date
FROM reservations r
WHERE r.reserved_by = %s AND r.reservation_date >= %s;
"""
cursor.execute(query, (email, todays_date))
reservation_dates = cursor.fetchall()
# Prepare the response data
if reservation_dates:
response_data = [res[0].strftime("%Y-%m-%d") for res in reservation_dates]
return func.HttpResponse(
body=json.dumps(response_data),
status_code=200,
mimetype="application/json",
)
else:
return func.HttpResponse(body=json.dumps([]), status_code=200)
except Exception as e:
logging.error(f"Error fetching reservation dates for email {email}: {str(e)}")
return func.HttpResponse(
"An error occurred while processing the request.", status_code=500
)
finally:
if cursor:
cursor.close()
if connection:
connection.close()
@app.route(route="reservations", methods=["POST"], auth_level=func.AuthLevel.ANONYMOUS)
def reserveDesks(req: func.HttpRequest) -> func.HttpResponse:
# Parse the JSON body
request_body = req.get_json()
if not request_body:
return func.HttpResponse("Invalid or missing JSON body.", status_code=400)
# Extract `userEmail`
employee_email = request_body.get("employee")
# Extract `reservations`
reservations = request_body.get("reservations", [])
# Validate presence of required fields
if not employee_email or not reservations:
return func.HttpResponse(
"Missing 'employee' or 'reservations' in the JSON body.",
status_code=400,
)
# Database connection
try:
connection = psycopg2.connect(
host=DB_HOST,
port=DB_PORT,
database=DB_NAME,
user=DB_USER,
password=DB_PASSWORD,
)
cursor = connection.cursor()
except Exception as e:
logging.error(f"Error connecting to the database: {str(e)}")
return func.HttpResponse("Database connection failed.", status_code=500)
# Connect to database and fetch reservations for the given date
try:
for reservation in reservations:
desk_id = reservation.get("desk_id")
reservation_date = reservation.get("reservation_date")
if not desk_id or not reservation_date:
return func.HttpResponse(
"Each reservation must include 'deskId' and 'reservationDate'.",
status_code=400,
)
checkIfSameReservationAlreadyExistsQuery = """
SELECT r.id, r.desk_id, r.reserved_by, r.reservation_date
FROM reservations r
WHERE r.reservation_date = %s AND r.desk_id = %s;
"""
cursor.execute(
checkIfSameReservationAlreadyExistsQuery, (reservation_date, desk_id)
)
reservations = cursor.fetchall()
if reservations and len(reservations) > 0:
response_data = [
{
"id": res[0],
"desk_id": res[1],
"reserved_by": res[2],
"reservation_date": res[3].strftime("%Y-%m-%d"),
}
for res in reservations
]
return func.HttpResponse(
body=json.dumps(response_data),
status_code=409,
mimetype="application/json",
)
# Insert reservation into the database
insert_query = """
INSERT INTO reservations (desk_id, reserved_by, reservation_date)
VALUES (%s, %s, %s)
"""
cursor.execute(insert_query, (desk_id, employee_email, reservation_date))
# Commit the transaction
connection.commit()
except Exception as e:
logging.error(f"Error fetching reservations: {str(e)}")
return func.HttpResponse(
"An error occurred while processing the request.", status_code=500
)
finally:
if cursor:
cursor.close()
if connection:
connection.close()
# Return success response
return func.HttpResponse(
f"Successfully created {len(reservations)} reservations for {employee_email}.",
status_code=201,
)
@app.route(
route="reservations", methods=["DELETE"], auth_level=func.AuthLevel.ANONYMOUS
)
def cancelReservations(req: func.HttpRequest) -> func.HttpResponse:
# Get reservation_ids from query parameter
reservation_ids_str = req.params.get("reservation_ids")
if not reservation_ids_str:
return func.HttpResponse(
"Please provide reservation_ids as a query parameter.", status_code=400
)
try:
# Split the reservation_ids and convert them into a list of integers
reservation_ids = list(map(int, reservation_ids_str.split(",")))
# Connect to database and delete the reservations
connection = psycopg2.connect(
host=DB_HOST,
port=DB_PORT,
database=DB_NAME,
user=DB_USER,
password=DB_PASSWORD,
)
cursor = connection.cursor()
placeholders = ", ".join("%s" for _ in reservation_ids)
# Query to delete the reservations by IDs
query = f"""
DELETE FROM reservations
WHERE id IN ({placeholders});
"""
cursor.execute(query, reservation_ids)
connection.commit()
# Check how many rows were deleted
deleted_rows = cursor.rowcount
if deleted_rows > 0:
return func.HttpResponse(
f"Successfully deleted {deleted_rows} reservation(s).", status_code=200
)
else:
return func.HttpResponse(
"No reservations found with the provided IDs.", status_code=404
)
except Exception as e:
logging.error(f"Error deleting reservations: {str(e)}")
return func.HttpResponse(
"An error occurred while processing the request.", status_code=500
)
finally:
if cursor:
cursor.close()
if connection:
connection.close()
Подробнее здесь: https://stackoverflow.com/questions/792 ... ite-workin
Почему мое развертывание функций Azure не работает через GitLab CI/CD, несмотря на работу с расширением Azure Function C ⇐ Python
Программы на Python
1734436372
Anonymous
Я создал несколько функций Azure с помощью Python (3.9), и у меня возникли проблемы с настройкой CI/CD в репозитории GitLab, где находится код. Этот репозиторий находится на виртуальной машине Azure, не уверен, что это актуально. Функции подключаются к базе данных Azure PostgreSQL, как вы увидите далее в коде.
.gitlab-ci.yml запускает 3 задания: 2 первых создают и развертывают React. app в статические веб-приложения Azure. Третий успешно работает в конвейерах GitLab, но после этого я не вижу никаких функций на странице обзора приложения-функции Azure. Я также не вижу журналов ошибок в задании.
При использовании расширения VSCode основных инструментов функций Azure для развертывания непосредственно в приложении-функции Azure развертывание проходит успешно, и функции отображаются на странице обзора, и они работают правильно, подключаются к базе данных и все такое. Я также могу запускать и тестировать функции локально.
Вот различные фрагменты кода. Некоторая информация будет опущена в целях конфиденциальности, например URL-адреса
.gitlab-ci.yml:
stages:
- build-react
- deploy-react
- deploy-functions
# Build the React app
build-react:
stage: build-react
image: registry.gitlab.com/static-web-apps/azure-static-web-apps-deploy
script:
- cd app-frontend
- npm install
- VITE_FUNCTIONS_BASE_URL=azurewebsites-url npx vite build
artifacts:
paths:
- app-frontend/dist
only:
- main
# Deploy the React app to Azure Static Web Apps
deploy-react:
stage: deploy-react
image: registry.gitlab.com/static-web-apps/azure-static-web-apps-deploy
script:
- cd app-frontend
- npm install -g @azure/static-web-apps-cli
- swa deploy --env production --deployment-token token --output-location ./dist
only:
- main
deploy-functions:
stage: deploy-functions
image: mcr.microsoft.com/azure-functions/python:3.0 # Azure Functions image
script:
- set -e
- cd azure_functions # Navigate to the folder containing your Azure Function app
- zip -r ../my-functions.zip . # Create a zip file of only the contents of the azure_functions folder
- cd .. # Navigate back to the root directory
- echo "Zipped successfully"
- apt-get update && apt-get install -y ca-certificates curl apt-transport-https lsb-release gnupg
- curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash
- echo "Deploying to Azure..."
- az login --service-principal -u $AZURE_CLIENT_ID -p $AZURE_CLIENT_SECRET --tenant $AZURE_TENANT_ID
- az functionapp deployment source config-zip --resource-group $AZURE_RESOURCE_GROUP --name $AZURE_FUNCTIONAPP_NAME --src ./my-functions.zip
only:
- main
requirements.txt:
azure-functions
psycopg2-binary
function_app.py:
import json
import azure.functions as func
import logging
from datetime import datetime
import psycopg2
import os
# Database connection details
DB_HOST = os.getenv("DB_HOST")
DB_NAME = os.getenv("DB_NAME")
DB_USER = os.getenv("DB_USER")
DB_PASSWORD = os.getenv("DB_PASSWORD")
DB_PORT = os.getenv("DB_PORT")
app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
@app.route(route="desks", methods=["GET"], auth_level=func.AuthLevel.ANONYMOUS)
def getDesks(req: func.HttpRequest) -> func.HttpResponse:
# Connect to database and fetch reservations for the given date
try:
connection = psycopg2.connect(
host=DB_HOST,
port=DB_PORT,
database=DB_NAME,
user=DB_USER,
password=DB_PASSWORD,
)
cursor = connection.cursor()
# Query to get all desks
query = """
SELECT d.id
FROM desks d;
"""
cursor.execute(query)
desks = cursor.fetchall()
# Prepare the response data
if desks:
response_data = [{"id": desk[0]} for desk in desks]
return func.HttpResponse(
body=json.dumps(response_data),
status_code=200,
mimetype="application/json",
)
else:
return func.HttpResponse("No desks found", status_code=200)
except Exception as e:
logging.error(f"Error fetching desks: {str(e)}")
return func.HttpResponse(
"An error occurred while processing the request.", status_code=500
)
finally:
if cursor:
cursor.close()
if connection:
connection.close()
@app.route(route="reservations", methods=["GET"], auth_level=func.AuthLevel.ANONYMOUS)
def getReservationsForDate(req: func.HttpRequest) -> func.HttpResponse:
# Get the date from the query parameter (expected format: YYYY-MM-DD)
reservation_date = req.params.get("date")
if not reservation_date:
return func.HttpResponse(
"Please provide a 'date' query parameter (YYYY-MM-DD).", status_code=400
)
try:
# Convert string date to a date object for validation
reservation_date = datetime.strptime(reservation_date, "%Y-%m-%d").date()
except ValueError:
return func.HttpResponse(
"Invalid date format. Please provide a valid date in YYYY-MM-DD format.",
status_code=400,
)
# Connect to database and fetch reservations for the given date
try:
connection = psycopg2.connect(
host=DB_HOST,
port=DB_PORT,
database=DB_NAME,
user=DB_USER,
password=DB_PASSWORD,
)
cursor = connection.cursor()
# Query to get all reservations for the provided date
query = """
SELECT r.id, r.desk_id, r.reserved_by, r.reservation_date
FROM reservations r
WHERE r.reservation_date = %s;
"""
cursor.execute(query, (reservation_date,))
reservations = cursor.fetchall()
# Prepare the response data
if reservations:
response_data = [
{
"id": res[0],
"desk_id": res[1],
"reserved_by": res[2],
"reservation_date": res[3].strftime("%Y-%m-%d"),
}
for res in reservations
]
return func.HttpResponse(
body=json.dumps(response_data),
status_code=200,
mimetype="application/json",
)
else:
return func.HttpResponse(body=json.dumps([]), status_code=200)
except Exception as e:
logging.error(f"Error fetching reservations: {str(e)}")
return func.HttpResponse(
"An error occurred while processing the request.", status_code=500
)
finally:
if cursor:
cursor.close()
if connection:
connection.close()
@app.route(
route="reservation-dates", methods=["GET"], auth_level=func.AuthLevel.ANONYMOUS
)
def getDatesWithReservationForEmail(req: func.HttpRequest) -> func.HttpResponse:
email = req.params.get("email")
if not email:
return func.HttpResponse(
"Please provide an 'email' query parameter.", status_code=400
)
todays_date = datetime.today().date()
# Connect to database and fetch dates in which the email provided has reservations
try:
connection = psycopg2.connect(
host=DB_HOST,
port=DB_PORT,
database=DB_NAME,
user=DB_USER,
password=DB_PASSWORD,
)
cursor = connection.cursor()
# Query to get all reservations for the provided date
query = """
SELECT r.reservation_date
FROM reservations r
WHERE r.reserved_by = %s AND r.reservation_date >= %s;
"""
cursor.execute(query, (email, todays_date))
reservation_dates = cursor.fetchall()
# Prepare the response data
if reservation_dates:
response_data = [res[0].strftime("%Y-%m-%d") for res in reservation_dates]
return func.HttpResponse(
body=json.dumps(response_data),
status_code=200,
mimetype="application/json",
)
else:
return func.HttpResponse(body=json.dumps([]), status_code=200)
except Exception as e:
logging.error(f"Error fetching reservation dates for email {email}: {str(e)}")
return func.HttpResponse(
"An error occurred while processing the request.", status_code=500
)
finally:
if cursor:
cursor.close()
if connection:
connection.close()
@app.route(route="reservations", methods=["POST"], auth_level=func.AuthLevel.ANONYMOUS)
def reserveDesks(req: func.HttpRequest) -> func.HttpResponse:
# Parse the JSON body
request_body = req.get_json()
if not request_body:
return func.HttpResponse("Invalid or missing JSON body.", status_code=400)
# Extract `userEmail`
employee_email = request_body.get("employee")
# Extract `reservations`
reservations = request_body.get("reservations", [])
# Validate presence of required fields
if not employee_email or not reservations:
return func.HttpResponse(
"Missing 'employee' or 'reservations' in the JSON body.",
status_code=400,
)
# Database connection
try:
connection = psycopg2.connect(
host=DB_HOST,
port=DB_PORT,
database=DB_NAME,
user=DB_USER,
password=DB_PASSWORD,
)
cursor = connection.cursor()
except Exception as e:
logging.error(f"Error connecting to the database: {str(e)}")
return func.HttpResponse("Database connection failed.", status_code=500)
# Connect to database and fetch reservations for the given date
try:
for reservation in reservations:
desk_id = reservation.get("desk_id")
reservation_date = reservation.get("reservation_date")
if not desk_id or not reservation_date:
return func.HttpResponse(
"Each reservation must include 'deskId' and 'reservationDate'.",
status_code=400,
)
checkIfSameReservationAlreadyExistsQuery = """
SELECT r.id, r.desk_id, r.reserved_by, r.reservation_date
FROM reservations r
WHERE r.reservation_date = %s AND r.desk_id = %s;
"""
cursor.execute(
checkIfSameReservationAlreadyExistsQuery, (reservation_date, desk_id)
)
reservations = cursor.fetchall()
if reservations and len(reservations) > 0:
response_data = [
{
"id": res[0],
"desk_id": res[1],
"reserved_by": res[2],
"reservation_date": res[3].strftime("%Y-%m-%d"),
}
for res in reservations
]
return func.HttpResponse(
body=json.dumps(response_data),
status_code=409,
mimetype="application/json",
)
# Insert reservation into the database
insert_query = """
INSERT INTO reservations (desk_id, reserved_by, reservation_date)
VALUES (%s, %s, %s)
"""
cursor.execute(insert_query, (desk_id, employee_email, reservation_date))
# Commit the transaction
connection.commit()
except Exception as e:
logging.error(f"Error fetching reservations: {str(e)}")
return func.HttpResponse(
"An error occurred while processing the request.", status_code=500
)
finally:
if cursor:
cursor.close()
if connection:
connection.close()
# Return success response
return func.HttpResponse(
f"Successfully created {len(reservations)} reservations for {employee_email}.",
status_code=201,
)
@app.route(
route="reservations", methods=["DELETE"], auth_level=func.AuthLevel.ANONYMOUS
)
def cancelReservations(req: func.HttpRequest) -> func.HttpResponse:
# Get reservation_ids from query parameter
reservation_ids_str = req.params.get("reservation_ids")
if not reservation_ids_str:
return func.HttpResponse(
"Please provide reservation_ids as a query parameter.", status_code=400
)
try:
# Split the reservation_ids and convert them into a list of integers
reservation_ids = list(map(int, reservation_ids_str.split(",")))
# Connect to database and delete the reservations
connection = psycopg2.connect(
host=DB_HOST,
port=DB_PORT,
database=DB_NAME,
user=DB_USER,
password=DB_PASSWORD,
)
cursor = connection.cursor()
placeholders = ", ".join("%s" for _ in reservation_ids)
# Query to delete the reservations by IDs
query = f"""
DELETE FROM reservations
WHERE id IN ({placeholders});
"""
cursor.execute(query, reservation_ids)
connection.commit()
# Check how many rows were deleted
deleted_rows = cursor.rowcount
if deleted_rows > 0:
return func.HttpResponse(
f"Successfully deleted {deleted_rows} reservation(s).", status_code=200
)
else:
return func.HttpResponse(
"No reservations found with the provided IDs.", status_code=404
)
except Exception as e:
logging.error(f"Error deleting reservations: {str(e)}")
return func.HttpResponse(
"An error occurred while processing the request.", status_code=500
)
finally:
if cursor:
cursor.close()
if connection:
connection.close()
Подробнее здесь: [url]https://stackoverflow.com/questions/79287753/why-is-my-azure-functions-deploy-not-working-through-gitlab-ci-cd-despite-workin[/url]
Ответить
1 сообщение
• Страница 1 из 1
Перейти
- Кемерово-IT
- ↳ Javascript
- ↳ C#
- ↳ JAVA
- ↳ Elasticsearch aggregation
- ↳ Python
- ↳ Php
- ↳ Android
- ↳ Html
- ↳ Jquery
- ↳ C++
- ↳ IOS
- ↳ CSS
- ↳ Excel
- ↳ Linux
- ↳ Apache
- ↳ MySql
- Детский мир
- Для души
- ↳ Музыкальные инструменты даром
- ↳ Печатная продукция даром
- Внешняя красота и здоровье
- ↳ Одежда и обувь для взрослых даром
- ↳ Товары для здоровья
- ↳ Физкультура и спорт
- Техника - даром!
- ↳ Автомобилистам
- ↳ Компьютерная техника
- ↳ Плиты: газовые и электрические
- ↳ Холодильники
- ↳ Стиральные машины
- ↳ Телевизоры
- ↳ Телефоны, смартфоны, плашеты
- ↳ Швейные машинки
- ↳ Прочая электроника и техника
- ↳ Фототехника
- Ремонт и интерьер
- ↳ Стройматериалы, инструмент
- ↳ Мебель и предметы интерьера даром
- ↳ Cантехника
- Другие темы
- ↳ Разное даром
- ↳ Давай меняться!
- ↳ Отдам\возьму за копеечку
- ↳ Работа и подработка в Кемерове
- ↳ Давай с тобой поговорим...
Мобильная версия